# Polymesh Developer Documentation (full text)
> Full concatenated markdown of the Polymesh conceptual/developer docs. For the SDK API, see /llms.txt or https://github.com/PolymeshAssociation/polymesh-sdk
This file contains all documentation content in a single document following the llmstxt.org standard.
## Polymesh Wallet
## Introduction
The **Polymesh Wallet** is a **Google Chrome extension** that securely holds your account information and private signing keys. The Polymesh Wallet lets you manage accounts and interact with Polymesh and Polymesh decentralized apps (dApps).
Currently, it's available as a Chrome extension, supporting Google Chrome and Chromium-based browsers (such as Brave and Microsoft Edge).
Get the Polymesh Wallet extension [here](https://chrome.google.com/webstore/detail/polymesh-wallet/jojhfeoedkpkglbfimdfabpdfjaoolaf) or search for "Polymesh Wallet" in the Chrome Store and click "Add to Chrome."

Once installed, find the Polymesh icon in the plugins tray. It may appear under the plugin icon if not pinned. Click it to launch the "welcome screen."

## Create a New Account

Accept the Privacy Policy and Terms of Use, then click `Create new account`.
You will be presented with a 12-word mnemonic, which is a recovery phrase that can be used to regenerate the private signing key stored in the wallet. It's essential to store this phrase in a safe and confidential location to retain access to your key.

On the next screen, you'll need to enter the words **_in the correct order_** to confirm you have recorded the phrase accurately.

Check the box to confirm that you've saved the recovery phrase, press `Continue`, and select the words in the correct order.
Give your account a name and set a password. This password will help protect your account and encrypt your keys in local storage.

Your new account is now created with one unassigned key.

At the top of the window, you can see which network you're connected to by default. Here, we're connected to Testnet.
:::tip
- When connected to Mainnet, addresses will start with a **2**.
- When connected to Testnet, addresses will start with a **5**.
:::
:::note Hardware wallet signing
The Polymesh Wallet can also sign with a Ledger hardware wallet instead of a locally stored key. See the [Ledger Hardware Wallet](/getting-started/keys/ledger) guide, including [Choosing a Ledger App](/getting-started/keys/ledger#choosing-a-ledger-app) for the available Ledger applications.
:::
## Links
- [Polymesh Wallet](https://chrome.google.com/webstore/detail/polymesh-wallet/jojhfeoedkpkglbfimdfabpdfjaoolaf)
- [Polymesh Portal](https://portal.polymesh.network/)
---
## Ledger Hardware Wallet
## Overview
As an alternative to storing your password-encrypted private key in a browser wallet extension, a Ledger hardware wallet can be used to manage your private key offline. Ledger devices are hardware wallets that keep your secret secured on a physical device that does not expose it to your computer or the internet.
Ledger apps are compatible with the Ledger Nano S+, Nano X, Flex and Stax devices. (The discontinued Ledger **Nano S** is not recommended — it lacks the memory to support setting session keys and the full transaction set.)
From Polymesh v8, the chain supports **Merkleized metadata** signing (the `CheckMetadataHash` transaction extension — see the [chain changelog](/development/changelogs/v7.4-to-v8.0/other-runtime-changes#transaction-extension-checkmetadatahash-merkleized-metadata) for the technical detail), which lets a Ledger device verify and display full, human-readable Polymesh transaction details even when using a generic app. This means you can now use Polymesh with more than one Ledger application — see [Choosing a Ledger App](#choosing-a-ledger-app) below. It also means smoother chain upgrades going forward: because the device checks transaction details against the runtime's own metadata rather than a fixed set of transaction formats built into the app, newly added or changed transaction types are typically understood and displayed correctly without requiring a Ledger app update.
## Choosing a Ledger App
Three Ledger applications work with Polymesh, listed under these names in the Ledger Live app catalog:
- **Polymesh** _(recommended)_ — the Polymesh-branded application, developed by [Zondax](https://github.com/Zondax/ledger-polymesh) and published at [LedgerHQ/app-polymesh](https://github.com/LedgerHQ/app-polymesh). It maintains the dedicated **Polymesh key derivation path** (SLIP-44 coin type `595`, i.e. paths of the form `m/44'/595'/…`), so the addresses it produces match those generated by the legacy Polymesh ledger app and other Polymesh-native tooling. It also supports the v8 Merkleized-metadata signing described above. This is the preferred app for most users, and required if you want your Ledger addresses to line up with existing Polymesh keys.
- **Polkadot (DOT)** — the generic Polkadot application. Thanks to v8's Merkleized-metadata support, it can sign and clearly display Polymesh transactions even though it isn't Polymesh-specific. It is a good option if you are coming into the ecosystem fresh, or already manage Polkadot-ecosystem accounts with it and prefer a single app. It uses the **Polkadot derivation path** (SLIP-44 coin type `354`, i.e. paths of the form `m/44'/354'/…`), so it derives **different addresses** from the same recovery phrase than the Polymesh app does — see the warning below.
- **Polkadot Migration** — a general-purpose Ledger app that isn't locked to one chain's SLIP-44 coin type; wallets and tooling can direct it to derive keys for whichever coin type they specify. It exists ecosystem-wide to help users of chain-specific Ledger apps migrate to the generic Polkadot app. Because Polymesh keeps its own dedicated, Merkleized-metadata-compatible app, most Polymesh users don't need this app — use the **Polymesh** app directly instead.
:::warning Derivation path determines your addresses
The **Polymesh** app (coin type `595`) and the generic **Polkadot (DOT)** app (coin type `354`) use **different derivation paths**, so the **same recovery phrase produces different addresses** under each. If you set up a key under one app and later connect using the other, you will see different accounts and may think your funds are missing — they are safe, but under the other app's addresses. Decide which app you will use up front, and stick with it; the wallet setting described below must match the app you have open on the device.
:::
:::note
The addresses your Ledger produces still need to be onboarded like any other key: a DID is only required for identity and asset related transactions, not for basic POLYX transfers or staking. See [Onboarding](/getting-started/onboarding).
:::
## Requirements
Here is a list of what you will need before starting:
- A Ledger Nano S+, X, Flex or Stax device (the discontinued Nano S is not recommended).
- The latest firmware installed.
- Ledger Live is installed and updated to the latest version.
- The Polymesh Wallet browser extension - this can be downloaded from the [Chrome Store](https://chrome.google.com/webstore/detail/polymesh-wallet/jojhfeoedkpkglbfimdfabpdfjaoolaf).
- Or a Chromium-based web browser to access the [Polymesh App](https://mainnet-app.polymesh.network/).
:::note
Other Substrate based wallets may support importing of Polymesh keys from Ledger devices. Refer to the wallets documentation to determine if Ledger devices are supported.
:::
## Installing the Ledger Application on Your Device
- Open the "My Ledger" tab in Ledger Live.
- Connect and unlock your Ledger device.
- Allow the Ledger Live to connect by confirming on your device.
- Search the app catalog for the app you want to use and select "Install":
- **Polymesh** — the recommended, Polymesh-branded app (coin type `595`).
- **Polkadot (DOT)** — the generic Polkadot app, if you prefer to use it (coin type `354`).
- **Polkadot Migration** — not needed for typical Polymesh use; see [Choosing a Ledger App](#choosing-a-ledger-app) above.

:::tip Which app should I open?
Whichever app you installed and set up your key with is the one you must open on the device when signing. If you set up your key under the **Polymesh** app, open the **Polymesh** app to sign; if you set it up under **Polkadot (DOT)**, open that one. Opening a different app will surface different addresses (see the derivation-path warning above).
:::
## Using via the Polymesh Wallet
The [Polymesh Wallet extension](/getting-started/keys/polymesh-wallet) supports signing with Ledger Hardware Wallets. This allows you to use your Ledger to sign transactions in any application compatible with the Polymesh wallet.
### Selecting a Ledger App in the Wallet
The wallet needs to know which Ledger app you're using so it derives and signs with the matching addresses. To choose:
1. Open the Polymesh Wallet extension and click the **3-dot menu** in the upper right corner.
2. Select **Ledger Settings**.

3. In the **Ledger App** dropdown, choose either **Polymesh (recommended)** or **Polkadot Generic** — matching whichever app you installed and intend to use on your device.

4. Click **Save**.
:::note Legacy app on older networks
This setting only applies when connected to a network running Polymesh v8 or later (`spec_version` 8,000,000 and above). If you connect to a network on an older spec version, the wallet automatically falls back to the original, pre-v8 dedicated Polymesh Ledger app (the "Legacy" app) regardless of what's selected here.
:::
Once installed, open the app you set up your key with (for most users, the **Polymesh** app) on your Ledger device before connecting.
:::tip
If you don't enter the relevant Ledger app first, you'll see a screen saying "Your Ledger is not connected."
:::
In the Polymesh wallet, instead of selecting `Create new account`, you can select `Connect your Ledger` when setting up your first wallet or click `+ Add a key` and then `Connect Ledger device` if you've previously set up a key.

If prompted by a browser popup, select your hardware wallet and click “Connect.”
You can then name your Ledger key and select `Continue` to import it to your wallet. The Advanced settings on the import screen let you choose an alternate derivation path, enabling multiple keys from the same Ledger device. The default for the first key import is Account type 0, Address index 0 - think of it as a formula for generating child keys. Subsequent key imports will increment the Account type automatically to use a different key from the same device.
:::note
If using a custom derivation path it is important to remember the **type** and **index** used so you can reconnect it should it become disconnected.
:::

Now, when signing transactions with this key, you must connect your Ledger, open the app you set up the key with (the **Polymesh** app for most users), and you'll be prompted to confirm the transaction with your Ledger device.
:::info
When imported, Ledger keys are assigned the genesis hash of the chain selected in the wallet at the time of import (i.e., Mainnet or Testnet). If the key is intended for use on Mainnet ensure mainnet is selected in the top left corner of the wallet before adding a Ledger key. It's best not to share keys across Mainnet and Testnet, as some dApps may filter by genesis hash and not display keys imported for Testnet on a Mainnet instance, and vice versa.
:::
## Using via the Polymesh App
Some applications may support connecting your ledger device directly without the need for a compatible browser extension. One example is the [Polymesh App](https://mainnet-app.polymesh.network/) which is a Polymesh specific fork of the [Polkadot.js App](https://github.com/polkadot-js/apps).
### Loading Your Account
The functionality to use a Ledger device directly is currently gated behind a feature setting that you will need to turn on.
To turn on support for Ledger devices, go to the [Settings](https://mainnet-app.polymesh.network/#/settings) tab in the Polymesh App. Find the option for attaching Ledger devices and switch the option from the default "Do not attach Ledger devices" to "Attach Ledger via WebUSB".

Click _"Save"_ to keep your settings.
Now when you go to the [Accounts](https://mainnet-app.polymesh.network/#/accounts) tab you will see a new button that says _"From Ledger"_.
Ensure that:
1. The Ledger device connected to your computer
2. The Ledger device is unlocked
3. The relevant Ledger application (the **Polymesh** app for most users) is open on the device.
4. Other apps that may be using the Ledger Device, such as Ledger Live are closed.
Once the above has been confirmed click the From Ledger button.

A popup will appear asking you to select an key name and derivation path.

The first option lets you select an account type. You can have multiple keys on a single Ledger device. The second dropdown lets you pick a index. The combination of both represent the key derivation path. If in doubt, pick the first option for both. 0 / 0 is a good default.
Once you click "Save" to confirm your selection, you may be prompted to confirm the USB connection through a popup like the one below when adding the Ledger device for the first time:

Select your device from the list of devices and then click the "Connect" button.
You should now see the new key listed on the accounts page under the "hardware" section.
You can now use this key to interact with Polymesh on Polymesh App. If you select your Ledger key when submitting a transaction, you will be prompted to sign it on your Ledger. The Ledger device must be attached when submitting a transaction from your Ledger key.
## Links
- [Ledger Polymesh App GitHub](https://github.com/LedgerHQ/app-polymesh) — the published Polymesh Ledger app, developed by [Zondax](https://github.com/Zondax/ledger-polymesh)
- [Ledger Polymesh App support page](https://support.ledger.com/article/4641803176221-zd)
- [Polymesh Support Requests](https://polymesh.network/contact-us)
---
## Key Generation
Polymesh supports a range of compatible wallets that leverage public-key cryptography, with support for Ed25519, SR25519, and ECDSA keys. These wallets enable you to manage your Polymesh account, securely store private keys, and interact with the Polymesh blockchain.
Typically, blockchain wallets are either **hot wallets** (connected to the internet, like browser extensions or mobile apps) or **cold wallets** (offline, like hardware devices). With **non-custodial wallets** where only you control your private keys keeping your private key and recovery phrase secure is essential, as access to these is required to manage your account.
Below is a selection of **Polymesh-compatible, non-custodial wallet options** to help you get started.
:::info
If you're unsure which wallet to choose, simply use the [**Polymesh Wallet**](/getting-started/keys/polymesh-wallet) extension as a reliable starting point.
:::
### Hot Wallets (Browser Extensions)
- [Polymesh Wallet](/getting-started/keys/polymesh-wallet)
- [Talisman Wallet](https://www.talisman.xyz/)
- [Subwallet](https://subwallet.app/)
- [Polkadot.js Wallet](https://polkadot.js.org/extension/)
### Hot Wallets (Smartphones)
- [Nova Wallet](https://novawallet.io/)
- [Subwallet](https://subwallet.app/)
### Cold Wallets
- [Ledger Hardware Wallet](/getting-started/keys/ledger/)
---
## Onboarding
## Overview
Polymesh users who participate in **asset** and **identity** related transactions must have an on-chain identity (DID). Accounts used solely for POLYX transfers and staking (excluding validator activities) are not subject to this requirement.
From Polymesh v8, onboarding is to receive a DID and supports two paths:
- **Self-registration**: The user calls `identity::self_register_did` for their own key and pays the transaction fee.
- **Registrar-assisted registration**: A permissioned DID registrar registers a DID for a user key using `identity::register_did`, and pays the transaction fee.
If you are new to Polymesh, a practical flow is:
1. Set up your wallet
2. Acquire POLYX
3. Complete DID onboarding when you want to use identity and asset related features
This helps reduce onboarding friction for users who only need basic POLYX transfers or staking. If you self-register a DID, you will need POLYX to pay the registration fee.
:::note
Onboarding to obtain a DID is not a substitute for issuer-specific KYC requirements on regulated assets. Asset issuers can enforce additional compliance requirements through claims and compliance rules.
:::
## Connect Wallet and Select Key
When you first create an account, it appears as an unassigned key, meaning no on-chain identity (DID) is linked yet.

Open the [Polymesh Portal Mainnet](https://portal.polymesh.network/) or [Polymesh Portal Testnet](https://portal.polymesh.live/), then connect your wallet and select the key you want to onboard.

:::note
You can also use "Manually enter a wallet key" for view-only access. To complete onboarding and sign transactions, use a key from a connected supported wallet.
:::
Once connected, the Overview page shows the selected key and its current identity status.

## Complete Onboarding (Self-Assign DID)
For keys that are not yet linked to an identity, Portal should show "This key is not linked to an identity" and a `Get On-Chain Identity (DID)` button.
Self-assigning a DID submits an on-chain transaction, where the user pays the transaction fee. Therefore the user must have some POLYX in the signing key before proceeding.
When the user clicks this action:
1. Portal prepares an `identity::self_register_did` transaction for the selected key.
2. The wallet signing interface opens and prompts the user to sign.
3. After the user signs, the transaction is submitted.
4. After successful execution, the key is linked to a new DID.
### Registrar-assisted onboarding (optional)
DID registrars are a permissioned network role approved through on-chain governance.
Platforms may use registrar-assisted onboarding when a third party should assign the DID, for example:
- to sponsor onboarding fees for users
- to support users who do not yet hold POLYX
- to keep onboarding managed by platform operations rather than end users
- to support platform models where available signing methods are intentionally restricted
In this flow, the registrar submits DID registration for the user key and pays the fee on the user's behalf.
### DID persistence across platforms
Once assigned, a DID is linked to the key on-chain, not to the platform that submitted the registration transaction.
- If that same key is later used in another wallet or platform, the DID remains valid.
- The registrar that originally assigned the DID cannot revoke that DID assignment through the registrar role.
This differs from historical CDD claims (pre v8), which were separate claim records and could be updated or revoked.
### Testnet onboarding
On the [Testnet instance of the Polymesh Portal](https://portal.polymesh.live/), onboarding is done by a DID registrar and seeds accounts with test POLYX for experimentation.
## Extrinsic reference
- `identity::self_register_did()`
- Permissionless: registers a DID for the caller's own key.
- `identity::register_did(target_account)`
- Registrar-gated: registers a DID for the target account. Callable only by an active DID registrar.
- `identity::cdd_register_did` and `identity::cdd_register_did_with_cdd`
- Deprecated in v8. No longer attach a `CustomerDueDiligence` claim; registrar-gated legacy paths retained temporarily for transition.
## Links
- [Polymesh Portal Mainnet](https://portal.polymesh.network)
- [Polymesh Portal Testnet](https://portal.polymesh.live)
---
## Getting Started Guide
# Getting Started with Polymesh
Welcome to Polymesh! This guide will walk you through the essential steps to begin using the Polymesh blockchain network. You can start with basic POLYX usage first, then complete DID onboarding when you want to use identity and asset related features.
## What You'll Need
1. [**A Compatible Wallet**](/getting-started/keys) - This is your gateway to interacting with the Polymesh network. The wallet stores your private keys and allows you to sign transactions securely. The [Polymesh wallet](/getting-started/keys/polymesh-wallet) browser extension is a good starting point for those starting out with Polymesh.
2. [**POLYX Tokens**](/polyx) - POLYX is the native utility token of the Polymesh blockchain. You'll need POLYX to:
- Pay for transaction fees
- Participate in network governance
- Stake tokens to help secure the network and earn rewards
- Signal on governance issues
3. [**Identity Onboarding (DID Registration)**](/getting-started/onboarding) - DID onboarding is required for **identity** and **asset** related transactions, but not for basic POLYX transfers or staking (except validator activities). A DID can be registered by the user directly or by a permissioned DID registrar. If self-registering, you will need POLYX to pay the transaction fee. Once your DID is registered, you can:
- Create, issue, and manage regulated assets on Polymesh
- Receive and manage claims to prove attributes about your identity
- Participate in asset transfers and settlements
- Access advanced network features such as compliance, confidential assets, and governance proposals
- Interact with identity-based protocols and services
- Be assigned permissioned roles
## Next Steps
Follow these steps in order to get started:
1. [Install a Polymesh Wallet](/getting-started/keys) - Set up your secure access to the network
2. Acquire POLYX - Get tokens to start participating in the network.
- Purchase from exchanges that list POLYX and transfer to your wallet. See [CoinGecko](https://www.coingecko.com/en/coins/polymesh#markets) or [CoinMarketCap](https://coinmarketcap.com/currencies/polymesh/#Markets) for a range of market options.
3. [Complete DID Onboarding](/getting-started/onboarding) - Register a DID for your signing key when you want to use identity and asset related features (self-registration or registrar-assisted).
:::tip Getting Started on Testnet
If you want to try Polymesh without using real assets, you can use the Testnet. After setting up your wallet, you can start with basic POLYX activity and complete DID onboarding when you want to test identity and asset features.
:::
:::note
Identity and asset related transactions on Polymesh require a signing key that is linked to a DID. POLYX transfers and staking (except validator activities) can be performed without a DID.
:::
---
## Architecture
## Overview
The Polymesh blockchain is optimised for regulated assets and markets. This is reflected in its approach to governance, security and consensus, as well as the base layer primitives provided directly by the network for asset origination and lifecycle management.
Polymesh is a public, permissioned blockchain. This means that anyone can run a node and check that the rules enforced by the network are being followed by all participants, and see all the public state secured by the blockchain, but that only certain entities (called operators) are allowed to run nodes that author new blocks, and vote on the finality of other blocks.
Polymesh is built on the [Substrate framework](https://substrate.io/), an open-source project, developed by by [Parity](https://www.parity.io/), that provides a fully customisable, modular and extendable framework for blockchain developers.
Polymesh has a native token, used for security and payments, [POLYX](/polyx/).
Polymesh provides core business logic and financial abstractions at its base layer, called primitives. These provide a rich set of embedded on-chain actions (called extrinsics) that a user can initiate, such as creating an asset, settling securities instructions or participating in on-chain governance.
Polymesh also supports Layer 2 [Smart Contract](../development/smart-contracts/) logic via `pallet-revive`, allowing network users to extend and leverage the base layer primitives to build on-chain financial and identity based protocols. `pallet-revive` is a dual-VM contract engine: Solidity compiled with `resolc` runs as native [PolkaVM](https://github.com/paritytech/polkavm) (a RISC-V based execution engine) bytecode with partial EVM compatibility, while Solidity compiled with the standard `solc` compiler produces EVM bytecode that runs in a separate Rust EVM implementation (`revm`) with full EVM compatibility. The executing VM is chosen from the uploaded bytecode, not from how the transaction is submitted — so EVM contracts can be deployed and called through ordinary Substrate transactions, as well as via standard Ethereum tooling routed through a dedicated JSON-RPC proxy. See [Smart Contracts](../development/smart-contracts/) for detail.
## Architecture

## Polymesh and Polymesh Private
While Polymesh is a public-permissioned blockchain, [Polymesh Private](/polymesh-private/) is a variant designed for deployment as a private blockchain among participating chain operators. There is a single Polymesh mainnet, but there can be many independent instances of Polymesh Private chains.
Polymesh Private shares much of Polymesh's functionality, with the following differences:
- Polymesh Private chains are intended to run among a limited set of peers on a private network.
- Polymesh Private does not use the [POLYX](/polyx/) token. While transactions may still incur fees, they are paid in a token specific to each Polymesh Private instance, which is not expected to have intrinsic value. Alternatively, Polymesh Private transactions can be configured to have no fees.
- The use of Polymesh Private is governed by a license; see [the Polymesh Private license details](https://github.com/PolymeshAssociation/polymesh-private?tab=readme-ov-file#license) for more information.
## Polymesh Pillars
Polymesh is tailored to the needs of regulated assets and global capital markets. This approach is guided by the four key pillars of Polymesh - Identity, Governance, Confidentiality and Compliance.
### Identity
[Identity](/identity/) is at the core of Polymesh. Polymesh uses DID-based onboarding where users can self-register a DID or use a permissioned DID registrar to assign a DID. Identity and asset related transactions in Polymesh require an on-chain identity, represented by a DID.
Identities provide attestation and key management. All users must act through an on-chain identity when interacting with identity and asset related transactions on Polymesh. Identities are referenced through DIDs (decentralised identifiers).
Identity attestations allow asset issuers to enforce compliance on-chain in real-time as assets are issued, traded and settled between different parties.
Identity key management allows users and organisations to flexibly manage their on-chain identities via primary and secondary keys and delegate asset management to other identities via external agent functionality.
An entity (a real world individual or organisation) can have multiple on-chain identities, allowing them to keep their overall positions segregated and confidential.
### Governance
Governance allows the chain to grow and develop. Polymesh has a sophisticated governance mechanism that combines signals from the broader community with technical experts (committees) and a governing council for actioning proposals. Polymesh Improvement Proposals (PIPs) can be proposed by any network user, and possible PIPs include network upgrades, setting the parameters of the network related to consensus and security and many other actions.
The Polymesh governance system is designed to further decentralise over time.
### Confidentiality
Confidentiality allows Polymesh users to maintain privacy over certain aspects of their securities transactions. The [Confidential Assets](/confidential-assets/) pallet is designed to allow confidentiality within asset transfers using zero-knowledge proofs, while still supporting regulated-market workflows such as receiver affirmation and asset-specific auditor/mediator compliance access. It is live on Testnet as a pre-release feature and not yet available on Mainnet.
### Compliance
Polymesh facilitates claim based compliance directly in its base layer primitives. Asset issuers can set flexible and extendable rules relating to the claims that their investors are required to have attached to their identity in order to either send or receive the asset. These rules can be combined to create complex transfer restrictions that are tailored to the assets specific type, jurisdiction and regulatory regime.
## Consensus
Polymesh provides financial primitives and business logic, built on top of a distributed storage ledger.
Updates to the ledger are processed across a decentralised network of Polymesh operator nodes. The consensus mechanism consists of three main components:
1. Operator Selection: Nominated Proof-of-Stake (NPoS) determines which permissioned operators will actively participate in block production and finalization. In this system:
- Node Operators (validators) indicate their intention to submit blocks and make their candidacy public
- Token holders participate as nominators by staking POLYX tokens behind operators they trust
- The network through the election algorithm distributes staked tokens to maximize economic security
- Operators with the highest backing are elected as active validators
- Both operators and nominators (currently disabled) face risk of slashing (loss of staked tokens) for operator misbehavior
2. Block Production: Elected operators participate in **BABE** (Blind Assignment for Blockchain Extension), which determines which operator can produce blocks in each time slot
3. Block Finality: These same operators also participate in **GRANDPA** (GHOST-based Recursive ANcestor Deriving Prefix Agreement), which provides rapid finality by having operators vote on chains rather than individual blocks
Polymesh is a permissioned network, and in order to run an operator node, the associated identity must first be permissioned through an on-chain [governance process](/governance/). These operators are typically regulated or licensed entities in their home jurisdiction, adding an additional layer of security through real-world accountability.
For more details on Consensus see:
- [Substrate Consensus Details](https://docs.substrate.io/learn/consensus/)
- [Polkadot Consensus Details](https://wiki.polkadot.network/docs/learn-consensus)
---
## Network Fees
## Overview
Fees on Polymesh are paid in the networks native token, [POLYX](/polyx/).
There are two types of network fee:
- Transaction Fees - always paid whether or not the transaction is successful
- Protocol Fees - only paid for successful transactions
## Transaction Fees
Every on-chain transaction in Polymesh must be paid for using POLYX. The cost of the operation is proportional to the computational and storage complexity of the action being performed and is set directly by the network.
In order to execute an on-chain transaction, a user must have sufficient POLYX associated with the key which is signing and submitting the transaction. If there isn't a sufficient balance, the network will fail the transaction.
Transaction fees go to the operator that produced the block containing the associated transaction. This means that operators are incentivised to help increase adoption of the network (as they receive a percentage of transaction fees from on-chain activity).
All transactions in Polymesh have a transaction fee associated with them. The magnitude of the fee is determined by:
- The computational cost of executing the transaction
- The memory/storage cost
- The size (in bytes) of the transaction input
The transaction fee is paid regardless of whether the transaction is successful or not.
To get an indication of current transaction fees, you can use block explorer tools like [Polymesh Subscan](https://polymesh.subscan.io/) to view recent transaction costs
## Protocol Fees
In addition to transaction fees, certain on-chain transactions carry additional fixed fees.
The transactions that carry these additional protocol fees are configurable and can be updated via the governance process - modifying both the transactions that carry these fees, as well as the fee amounts for each transaction.
Currently only one type of transaction carries additional fees - this is:
- registering a new ticker: this has a 25 POLYX fee. Ticker registration is optional and only required if you wish to have a unique ticker linked to your asset.
Both transaction and protocol fees are paid to the operator that produces a block that includes the relevant transactions.
Protocol fees are only paid by a user if their action is successful - for example if you try and register a ticker that has already been registered, you won't be charged the 25 POLYX protocol fee.
## Fee Subsidization
To reduce friction for users, Polymesh supports transaction fee subsidization where one account (the subsidizer) can pay fees on behalf of another account. This is particularly useful for businesses wanting to cover transaction costs for their users. For more details on this feature, see the [Subsidized Accounts](/accounts/subsidized) documentation.
---
## POLYX
## Overview
POLYX is the native utility token of the Polymesh network.
It serves several key functions:
- **Transaction Fees:** POLYX is required to pay for transaction fees on the network.
- **Staking:** Both operators (validators) and non-operators (nominators) can stake POLYX to help secure the network and earn rewards.
- **Governance:** POLYX holders can signal on governance issues and participate in network proposals.
- **Block Rewards:** New POLYX is minted only as block rewards, distributed to operators and stakers for supporting network security and consensus.
POLYX can be [staked](/polyx/tokenomics/) to receive a proportional share of block rewards. Operators may be required to stake a minimum bond to run an authoring node.
## Security
Nodes participating in the [consensus](/architecture#consensus) mechanism of Polymesh must stake POLYX. This stake is at risk if the operator misbehaves (e.g., goes offline or produces invalid consensus messages), incentivizing operators to maintain network reliability and rapid block finalization.
## Accessibility
POLYX is freely transferable between keys. Unlike asset and identity related transactions, POLYX transfers and staking (except validator activities) do not require identity onboarding or a DID.
---
## Staking
## Introduction
Polymesh uses a Nominate Proof-of-Stake (NPoS) [consensus](/architecture#consensus) mechanism that enables token holders to contribute to securing the network by bonding their POLYX and nominating it to trusted permissioned [Node Operators](/glossary#node-operator). In return, stakers earn rewards and gain influence over network governance. For definitions of key staking terms, please refer to the [glossary](/glossary/#staking--node-operations).
:::note
The Node Operator role on Polymesh is a permissioned role restricted to regulated entities approved by Polymesh and assigned an onchain permission.
:::
## Why Stake?
- Earn staking rewards for securing the chain
- Participate in network governance by actively backing node operators
- Support decentralization by distributing stake across multiple operators
## Staking as a Nominator
1. **Bonding**: [Nominators](/glossary#nominator) bond tokens that they wish to use for staking purposes. Once tokens are locked (bonded), they cannot be transferred without first unbonding, waiting for an unbonding period to elapse, and then withdrawing them so they become freely transferable. The key that holds the bonded tokens is known as the [Stash](/glossary#stash) key.
2. **Nominating**: Select node operators they wish to support (16 maximum).
3. **Election**: The system operates in [eras](/glossary#era). Prior to the commencement of a new era, an election of node operators occurs to determine the node operators for the next era. The election algorithm is designed to maximize the total POLYX at stake by electing the operators with the highest number of tokens supporting them.
4. **Rewards**: At the end of each era, rewards are distributed to node operators and nominators based on the performance of the node(s) their tokens were backing. Rewards are paid from newly minted tokens. The amount of newly minted tokens is determined by the chain's [reward curve](#reward-curve).
5. **Slashing**: Tokens staked by node operators are at risk of being lost if they misbehave. Nominator tokens are not currently subject to slashing, but that could change in the future.
:::note
The unbonding period on Polymesh mainnet is 28 days. This means that once you bond tokens and later initiate an unbonding request, you will have to wait for 28 days before you can withdraw your tokens.
:::
## Reward Curve
The diagram above shows how rewards are calculated based on the total amount of POLYX staked in the network. The curve depicts the **average** APR/APY across the chain and does not reflect rewards after operators' commissions are subtracted, which will reduce the amount received by a nominator.
The APY (Annual Percentage Yield) is based on automatically staking rewards and compounding returns.
The reward curve is designed to incentivize staking by increasing the rewards if the total amount of staked POLYX decreases. The maximum number of tokens that can be minted in a year is capped at 140 million POLYX. Hence, the maximum inflation rate decreases daily as the total supply of POLYX increases. This is reflected by the flat top on the inflation curve.
## How to Stake via the Polymesh Portal
The [Polymesh Portal](https://portal.polymesh.network/) offers a convenient way to stake on the Polymesh Blockchain. Before you get started, you must first have completed the initial steps described in the [Getting Started](/getting-started/) section. Once you have POLYX tokens, you can follow the steps below to stake:
1. Navigate to https://portal.polymesh.network/staking.
2. If you have not previously used the Polymesh Portal, you will be prompted to connect your wallet and authorize the Portal to interact with it. Review the information displayed by the wallet and accept the request to allow the Portal to read the keys stored in your wallet and submit transactions for signing.
3. Once your key is selected, click on the "Stake POLYX" button to start the staking process.

4. Complete details in the Stake POLYX form.
- Enter the amount of tokens you wish to stake.
- Select the node operators you wish to nominate (maximum of 16).
- If you do not wish to automatically re-stake rewards, uncheck the "Automatically stake rewards" checkbox.

:::warning
If you click the "Use max" button to bond your full available balance, you will not have any unlocked POLYX to cover any subsequent transaction fees. Only bond all your tokens if you understand that you will need to top up your key before you will be able to submit future transactions.
:::
5. Once all details are complete, click the "Stake" button.
6. Review the transaction details and approve it in your wallet to sign the transaction and submit it to the chain.
7. After the transaction completes, the staking page will update to show your staking information.

:::note
When you initially bond, you will see a note stating:
"Your tokens are bonded but not staked in this era. Your tokens should automatically start staking in the era following the next election of node operators. (max 28 hrs)".
This is because the election of node operators occurs at the start of each era and the staking process will commence after the next election. Your first reward payout will not be until the end of the era following the next election. This could be up to 52 hours after you initially bond your tokens.
:::
### Advanced Options
When initially configuring staking, you can click the "show advanced options" button to access additional staking options.
This includes:
- setting a reward destination for rewards. This can be set to automatically stake rewards, send rewards to the stash address, or send rewards to a separate address.
:::note
Newly bonded stashes no longer support a separate [Controller key](/glossary#controller) — the stash key itself always manages the bonded funds. A separate controller is a legacy pattern retained only for stashes bonded before this change, and their controller can still be reset to the stash key at any time.
:::
### Additional actions
Once you have staked your POLYX, you will have access to additional actions through updated buttons or by clicking on the 3 dots to the right of the "Learn how to stake" button. These include:
- **Bond More POLYX** - Increases the amount of POLYX you have bonded.
- **Change Nominations** - Update the node operators you are nominating.
- **Change Controller Address** - Reset a legacy separate controller back to the stash key. New bonds do not support setting a separate controller key.
- **Change Reward Destination** - Update the destination for rewards.
- **Remove Nominations** - Remove all nominations from the account.
- **Unbond POLYX** - Initiate the unbonding process for your staked tokens.
- **Rebond POLYX** - Rebond tokens that are in the process of unbonding that have not completed the waiting period.
- **Withdraw Unbonded** - Withdraw tokens that have completed the unbonding waiting period, to make them transferable by the stash.
:::note
Depending on the type of key selected (Controller or Stash), you may not have access to all of these options.
:::
## Additional Resources
- [Staking Guide](https://community.polymesh.live/hc/en-us/articles/12131464556060-How-to-stake-on-the-Polymesh-Portal)
- [Polymesh Portal](https://portal.polymesh.network/)
---
## Tokenomics
## Overview
POLYX is the native token of Polymesh and is used to stake and pay for transactions on the network.
POLYX provides security to the Polymesh network by:
- metering the cost of compute and storage through [transaction fees](../fees/) preventing Denial of Service attacks
- incentivising correct behaviour by operators by rewarding them for acting correctly, and penalizing them for downtime or inconsistent data.
## POLYX Total Supply
POLYX does not have a fixed maximum total supply. New tokens are minted at the end of every era to reward operators and stakers as part of the Proof of Stake consensus protocol, which are distributed to node operators and stakers. The rate at which new tokens are minted is determined by the Rewards Curve and is capped at a maximum of 140 million POLYX per year.
## Reward Curve
The rewards paid to operators and stakers on Polymesh varies based on the amount of POLYX currently being used to stake.
The ideal staking ratio will be initialised to 70% (i.e. 70% of the total supply of POLYX on Polymesh being staked), but can be modified via governance.
Whilst the staking ratio is lower than this amount, rewards are relatively high to encourage new stakers. Once we reach the ideal staking ratio, rewards drop off exponentially encouraging additional liquidity (i.e. unstaking).
For more details see: https://research.web3.foundation/Polkadot/overview/token-economics
## Fees
Transaction fees provide Polymesh with a spam prevention mechanism, preventing Denial of Service style attacks. The cost of a transaction (denominated in POLYX) is determined via its computation and storage costs. This is done by benchmarking each extrinsic and setting the POLYX fee accordingly.
Polymesh processes transactions on a first in, first out basis. There is no equivalent to "gas price" (as seen in Ethereum) and tipping (in order to have your transaction included earlier) is not allowed in Polymesh.
## Rewards and Penalties
Polymesh has a fixed upper limit of operator nodes which are chosen in each era to participate in the consensus protocol (GRANDPA and BABE) and earn rewards for themselves, and their stakers.
If there are more candidate operator nodes, than available slots, the operator nodes with the most stake will be chosen. This encourages competition amongst operators to attract stake, which translates to reasonable commissions, and emphasises the need to have an excellent reputation and track record of stable performance, which in turn promotes the stability and security of the Polymesh network.
Each operator can run multiple nodes, although there is a fixed upper limit on the number of nodes that a single permissioned operator identity can run. This prevents an operator from being able to capture the whole network, even if they capture a majority of the networks stake.
Operators can set a commission, although there is a global cap on the commission that each individual operator can set.
Regardless of their stake, each chosen operator has an equal chance of receiving rewards during an era. Rewards during an era are tracked via points for certain actions (e.g. creating a block, voting correctly on a block) and at the end of an era, these points are used to proportionally divide the eras reward between operators.
Once a reward has been calculated for a particular operators, the commission is first removed and paid directly to the operator. The remaining reward is then split proportionally across all accounts that have staked that particular operator, including the operator themselves if they have self-staked.
If an operator is off-line, or equivocates (votes on two contradictory blocks) they can be fined. The fine depends on a number of parameters, including the overal state of the network (e.g. how many other nodes are also offline). For more details see:
https://research.web3.foundation/Polkadot/security/slashing/amounts
---
## Identity Onboarding
## Overview
From Polymesh v8, onboarding on Polymesh is only based on DID registration. To access identity and asset related functionality, a key must be linked to an on-chain identity (DID).
Accounts used only for POLYX transfers and staking (excluding validator activities) do not require a DID.
## DID registration paths
Polymesh supports two onboarding paths:
1. **Self-registration**
- A user registers a DID for their own key by submitting `identity::self_register_did` and paying the transaction fee.
2. **Registrar-assisted registration**
- A permissioned DID registrar registers a DID for a target user key using `identity::register_did` and pays the transaction fee.
This two-path model lets users onboard directly while also supporting platforms that prefer managed onboarding for user experience reasons.
## DID registrars
DID registrars are permissioned identities that can register new DIDs for other keys.
Registrars are managed through on-chain governance (via PIP/governance process) and are intended for entities that want to offer simplified onboarding to users who may not yet hold POLYX for fees.
## register_did and self_register_did
`identity::self_register_did()` is fully permissionless: any account not already linked to an identity can call it to register a DID for its own key.
`identity::register_did(target_account)` is registrar-gated: only an active DID registrar can call it, to register a DID for a target key.
Important behavior in v8:
- DID existence is the onboarding requirement for identity and native asset related usage.
- Neither extrinsic supports secondary key onboarding.
- Legacy `cdd_register_did` and `cdd_register_did_with_cdd` remain available as deprecated, registrar-gated transition paths, but no longer attach a `CustomerDueDiligence` claim.
## CDD claim legacy context
The `CustomerDueDiligence` claim type remains available for compatibility, but it no longer serves as the functional onboarding gate in v8.
Asset issuers can continue to enforce issuer-specific KYC/compliance requirements using claims and compliance rules.
---
## Primary Keys
## Overview
A primary key is the ultimate controller of a Polymesh identity. Each identity must have exactly one primary key that serves as its root of authority, able to manage all aspects of the identity including assets, permissions, and key management.
:::warning
Primary keys cannot be replaced if lost — they can only be rotated to a new key through a controlled [authorization](/authorizations) process. The rotation is normally initiated by the primary key, but a [secondary key](/identity/advanced/secondary-keys) with `add_authorization` permission can also initiate it — which is what makes such a key usable as a recovery path if the primary key is lost. Secure storage and backup of primary keys is critical.
:::
## Supported Key Types
A primary key can be one of the following:
- A public/private key pair using:
- ED25519
- SR25519
- ECDSA (secp256k1)
- A [multisig](/identity/advanced/multisig) arrangement requiring multiple approvals
- A [smart contract](/development/smart-contracts) that implements key functionality
Each type has its own security and operational considerations:
- **Single Key Pairs**: Simplest to manage but rely on secure storage of a single private key
- **Multisigs**: Enhanced security through requiring multiple approvals but more complex operationally
- **Smart Contracts**: Enable programmatic control and complex logic but require careful implementation and testing
## Core Capabilities and Operations
A primary key has unrestricted access to all identity functions and cannot have its permissions limited. It can perform any operation the identity is authorized for.
### Functions exclusive to the primary key
A small set of secondary-key management functions can **only** be performed by the primary key. The runtime enforces this directly — it rejects these calls from any secondary key, regardless of that key's permissions:
- Setting or modifying secondary key permissions (`identity::set_secondary_key_permissions`)
- Adding secondary keys directly via `identity::add_secondary_keys_with_authorization`
- Removing secondary keys (`identity::remove_secondary_keys`)
- Freezing/unfreezing all secondary keys (`identity::freeze_secondary_keys` / `identity::unfreeze_secondary_keys`)
- Acting as a [multisig](/identity/advanced/multisig)'s admin or paying identity (the multisig `..._via_admin` / `..._via_payer` calls, which require the caller to be the primary key of the designated admin/payer identity)
### Operations the primary key can perform, but which are not exclusive to it
The primary key can also carry out identity-level operations such as:
- Creating [multisig](/identity/advanced/multisig) arrangements
- Transferring asset ownership between identities
- Granting [agent permissions](/asset-agents) for asset management
- Authorizing [portfolio custody](/portfolios/custody) transfers
- Initiating [primary key rotation](#key-rotation)
These are **not** restricted to the primary key. A [secondary key](/identity/advanced/secondary-keys) with the appropriate extrinsic permissions — plus permission to call `identity::add_authorization` where the operation uses the [authorization](/authorizations) system — can perform them too. Reserve them for the primary key as a matter of operational policy, not because the protocol requires it.
:::note
While primary keys can execute any transaction the identity is authorized for, it's recommended to use properly permissioned [secondary keys](/identity/advanced/secondary-keys) for routine operations and reserve the primary key for critical identity management functions.
:::
:::warning Extrinsic permissions delegate authority
While primary keys have unrestricted access, extrinsic (transaction) permissions granted to [secondary keys](/identity/advanced/secondary-keys) delegate the full authority of the calls they allow. A secondary key with `add_authorization` permission can create authorization requests for sensitive operations, including primary key rotation; more generally, a key permitted to call any permission, key, or authorization-management extrinsic can obtain or grant permissions broader than its own. `Assets`/`Portfolios` scoping does not contain this — those scopes bound only portfolio access (asset transfers) and external-agent actions, not identity-management authority. Carefully audit which secondary keys hold such permissions. See [Extrinsic Permissions Delegate the Authority of the Calls They Allow](/identity/advanced/secondary-keys#extrinsic-permissions-delegate-the-authority-of-the-calls-they-allow).
:::
## Key Management
### Security Best Practices
To protect your identity:
- Store the primary key in cold storage (offline)
- Use [secondary keys](/identity/advanced/secondary-keys) with appropriate permissions for day-to-day operations
- Maintain secure backups of primary key material
- Document key recovery procedures
- Consider using a [multisig](/identity/advanced/multisig) as your primary key for enhanced security
:::tip Key Recovery Option
If you're concerned about potential primary key loss or compromise, you can create a pending primary key rotation authorization request as a backup. While this creates another potential security vector if the backup key is compromised, it provides a recovery path if the primary key becomes inaccessible. Consider carefully securing both the primary and backup keys if using this approach.
:::
### Key Rotation
Primary keys can be rotated through a controlled [authorization](/authorizations) process that requires:
1. Creation of a rotation authorization (by primary key or secondary key with `add_authorization` permissions)
2. Acceptance by the new primary key
The rotation process can be done in two ways:
- Standard rotation to a new key using an authorization type of `RotatePrimaryKey`
- Rotation to an existing secondary key using an authorization of type `RotatePrimaryKeyToSecondary`
:::note
- During rotation, the current primary key remains in control until the process is complete. This ensures continuity of control over the identity.
- When an authorization request of type `RotatePrimaryKey` is enacted, the old primary key is detached from the onchain identity, preventing it from transacting. Any remaining POLYX held on that key will be locked until the key is associated with a new identity.
:::
## Advanced Configurations
### Multisig Primary Keys
Using a multisig as your primary key provides additional security through requiring multiple approvals for critical operations. When setting up a multisig primary key:
- Configure an appropriate number of required signatures
- Carefully select trusted signers
- Consider the operational impact of requiring multiple signatures
- Plan for key recovery scenarios
### Smart Contract Primary Keys
[Smart contracts](/development/smart-contracts) can serve as primary keys, enabling programmatic control over identity management. This configuration:
- Allows encoding complex management logic
- Can implement time-locks or other controls
- Must include logic to handle authorizations
- Should be thoroughly tested and audited
- Can implement upgrade mechanisms through proxy patterns
:::note
Smart contract upgrades require special consideration since Polymesh native assets are associated with identities and not the smart contract address. You can use key rotation as part of your upgrade strategy:
1. Deploy the new contract version
2. Use the old contract to authorize rotation to the new contract
3. Execute the rotation once the new contract is validated
This process ensures continuous access to identity-associated assets during upgrades.
:::
:::warning
Smart contract interactions with the chain runtime can potentially be broken in a major chain upgrade. To mitigate this:
- Implement a proxy pattern for contract upgrades
- Test thoroughly before each network upgrade
:::
## Guidelines for Usage
### When to Use Primary Key
Use your primary key for:
- Adding or removing secondary keys
- Modifying permissions of secondary keys
- Critical identity management operations
- Key rotation procedures
### When to Use Secondary Keys
Use [secondary keys](/identity/advanced/secondary-keys) for:
- Day-to-day operations
- Portfolio management
- Asset transactions
- Regular business activities
### Transaction Fee Considerations
- Primary keys need POLYX for transaction fees
- Cannot access POLYX balances of secondary keys
- Consider setting up [subsidized relationships](/accounts/subsidized) for fee management
### Limitations
- Only one primary key per identity
- Cannot be restricted in permissions
- Cannot be replaced without proper rotation
- Must maintain sufficient POLYX for transaction fees
- Cannot act as a secondary key for another identity
---
## Permissioned Roles
## Overview
Polymesh is a public network, so anyone can run a node, receive network and state updates and check that the blockchain is progressing as expected.
Different roles on the Polymesh network have different levels of permissioning required, all managed through the on-chain [governance](/governance/) process.
## Network Users
Network users interact with Polymesh in various ways, from transferring POLYX to managing identities and assets. Any user who wishes to manage identities and assets on Polymesh must complete [identity onboarding](/identity/onboarding/) so their key is linked to a DID. With a DID, a user can manage their identity, create and manage assets, settle transfer instructions, and access advanced network features.
:::note
Identity onboarding (DID linkage) is not required for POLYX transfers or staking, except for validator activities. This makes basic token operations accessible to all users, while asset and identity-related transactions remain permissioned.
:::
## Operators
Operator nodes are special nodes in the Polymesh network that author new blocks and participate in finalising the blockchain.
To run an operator node, a user must have a DID linked to a permissioned operator identity and be approved through the on-chain governance process. This is managed via a [Polymesh Improvement Proposal](/governance#polymesh-improvement-proposals). Operators play a critical role in maintaining network security and consensus.
## DID Registrars
DID registrars are trusted entities permitted to register DIDs for user keys. This allows platforms to offer streamlined onboarding where the registrar can pay the registration fee on behalf of the user.
Only explicitly permissioned identities can act as DID registrars. Registrars are added or removed via a PIP executed through the on-chain governance process.
## Committee Membership
Committees are specialized groups that can send PIPs directly to the Governing Council for ratification. There may be several committees, each with a different specialization and membership.
Committees manage their own membership, voting to add or remove members as needed.
## Governing Council Membership
The Governing Council is responsible for ratifying or rejecting PIPs and scheduling their execution. Like committees, the council manages its own membership through its established voting process.
---
## Secondary Keys
## Overview
Secondary keys in Polymesh provide a flexible and secure way to manage access to your identity on the blockchain. Secondary keys share the same DID as your Primary key but allow an identity owner to delegate specific permissions to different secondary keys, enabling more secure and granular control over your on-chain assets and actions.
Unlike the primary key (which has full control over an identity), secondary keys can be limited to specific functions, providing enhanced security and more flexible identity management.
## Features and Benefits
### Granular Permission Control
Secondary keys can be configured with specific permissions limited to:
- **Transactions**: Define which specific blockchain functions a key can execute
- **Portfolios**: Control which portfolios a key can access — this governs the key's ability to move and transfer assets
- **Assets**: Restrict which assets a key can administer as an external agent (asset-agent actions only — not ordinary transfers)
### Enhanced Security
- Limit exposure of your primary key by using secondary keys for day-to-day operations
- Create role-specific keys with minimal permissions needed for each function
- Freeze all secondary keys in case of security concerns while maintaining primary key control
### Operational Flexibility
- Delegate specific tasks to team members without sharing full identity control
- Create automated service accounts with limited permissions
- Maintain operational continuity while upholding security best practices
## Use Cases
Secondary keys enable a variety of operational structures and security models in Polymesh:
- **Secure treasury management** with primary key held by executives and limited secondary keys for daily operations
- **Separation of duties** by assigning different functional permissions to different team members
- **Operational continuity** by having multiple keys that can perform critical functions
- **Role-based access control** with permissions tailored to specific job functions
- **Compliance operations** with dedicated keys for compliance-related activities
- **Asset management** with specific keys for asset servicing and administration
- **Portfolio management** with different keys for different portfolio strategies
- **Staking management** by using a secondary key as a stash key, which directly manages its own staking operations for new bonds — see [Staking](/polyx/staking)
- **POLYX segregation** for financial controls and improved security by keeping balances separated
## Technical Overview
### Relationship to Primary Key
Every Polymesh identity has a single primary key that serves as the ultimate controller of the identity. The primary key:
- Has unrestricted access to all identity functions
- Can add, remove, and modify permissions for all secondary keys
- Cannot be restricted in its capabilities
- Can freeze/unfreeze all secondary keys associated with the identity
Secondary keys always exist in relationship to an identity and its primary key, with permissions explicitly granted by the primary key.
### Adding Secondary Keys
Secondary keys can be added to an identity through two main methods:
#### 1. Authorization Request
The primary key initiates an [authorization request](/authorizations) for a new secondary key:
- Using `identity::add_authorization` Primary key creates an authorization request of type `JoinIdentity` targeting the new key as the account to join the identity
- Authorization includes specific permissions for the new key
- Using the generated authorization ID the target account accepts the authorization using `identity::join_identity_as_key` to become a secondary key
This process involves consent from both parties:
- The identity owner (primary key) must initiate the authorization
- The secondary key owner must approve the assignment of their key to the identity
This ensures that no identity can claim control over a key without the key owner's explicit permission.
#### 2. Off-chain Authorization
For batch adding of multiple secondary keys or adding a key to an identity in a single transaction an off-chain authorization can be provided by the target secondary key(s):
- Primary key prepares authorization data (including permissions)
- Target accounts sign authorization data off-chain
- Primary key submits all signed authorizations at once
##### Off-chain Authorization Details
The signature generation process involves:
1. **Required payload components**:
- The requesting identity's DID
- The identity's current off-chain authorization nonce (retrieved via `identity::OffChainAuthorizationNonce`)
- Expiry timestamp for the authorization
2. **Payload Creation**:
- Concatenate the hex values (DID + nonce + expiry) without separators
- Wrap the concatenated string with `` and `` tags to form the final payload
3. **Signing**:
- Sign the wrapped payload using the future secondary key
- The resulting signature must be provided when adding the secondary key
:::note
Off-chain authorization signatures provide a secure way to prove that the new key consents to becoming a secondary key without requiring preliminary on-chain transactions. The payload data must be wrapped with `` and `` tags before signing.
:::
This method enables bulk addition of secondary keys without requiring each key to submit separate transactions to accept joining the identity.
### Secondary Key Permissions
Permissions for secondary keys are divided into three categories:
- **Extrinsics** (transactions): Define which blockchain functions (pallet and method) the key can call at all. This is the first gate every call passes through.
- Full access to all functions
- No access to any functions
- Access to specific modules and/or methods
- **Portfolios**: Control which of the identity's portfolios the key can access and manage. **This is what governs a key's ability to move and transfer assets** not directly held by their key — restricting a key's portfolios restricts which portfolio holdings it can move, send in a settlement, or otherwise transfer.
- Full access to all portfolios
- No access to any portfolios
- Access to specific portfolios only
- **Assets**: Control which assets the key can administer **as an external agent** — asset-agent actions such as issuance, redemption, compliance configuration, and controller (forced) transfers on assets the identity is an agent for.
- Full access to all assets
- No access to any assets
- Access to specific assets only
:::warning Asset permissions do not restrict transfers
Asset permissions apply **only** to external-agent actions on an asset. They do **not** restrict a key's ability to transfer that asset out of the identity's portfolios — ordinary transfers are governed entirely by **portfolio** permissions. If you need to stop a secondary key from moving or sending a particular holding, restrict its **portfolio** permissions; restricting its asset permissions will not prevent the transfer.
:::
How the three combine: a call must first be permitted by the key's **extrinsic** permissions. Beyond that, a call that moves assets in or out of a portfolio additionally checks the key's **portfolio** permissions, and a call performed as an asset's external agent additionally checks the key's **asset** permissions. Each dimension is enforced independently by the calls that use it — they are not intersected into a single "asset-within-portfolio" scope.
:::note
Some blockchain functions (like POLYX transfers and staking operations) don't involve assets, portfolios or an identity. These actions are always permitted.
:::
### Extrinsic Permissions Delegate the Authority of the Calls They Allow
Extrinsic (transaction) permissions control **which** calls a secondary key may submit. They do **not** sandbox the **effects** of those calls. This distinction is important: granting a secondary key permission to call an extrinsic grants it the full authority that extrinsic carries when it executes.
Some extrinsics create, assign, or modify key permissions, or otherwise link keys and authorizations to the identity. **Granting a secondary key permission to call such an extrinsic is equivalent to delegating that permission-management authority to the key.** A key permitted to call these calls can therefore acquire — or confer on another key — authority beyond the `Assets`/`Portfolios` scope it was originally assigned, because those scopes constrain only portfolio access and external-agent actions, not the identity-management authority carried by the call itself.
This is by design. Polymesh's permission model treats a key's extrinsic-permission set as a delegation of authority: if a key may call a permission- or key-management extrinsic, it may exercise that extrinsic's authority in full. Deciding which extrinsics to permission is therefore the identity owner's responsibility.
:::warning Granting call permissions can enable privilege escalation
Because extrinsic permissions delegate the authority of the calls they allow, granting a secondary key permission to call an identity, key, or authorization-management extrinsic (for example `add_authorization`, or any call that assigns or changes key permissions) can allow that key to obtain, or grant to another key, permissions broader than its own. This is intended behaviour of the delegation model, **not** a containment boundary.
When permissioning secondary keys:
- Apply least privilege: grant only the specific extrinsics a key needs for its role, and prefer a minimal, explicit extrinsic set over broad "all functions" (`Whole`) access.
- Treat permission to call any permission-, key-, or authorization-management extrinsic as granting that management authority itself.
- Do not rely on `Assets` or `Portfolios` scoping to contain a key that also holds such an extrinsic permission — those scopes bound only portfolio access and external-agent actions, not the authority a management call carries.
- Note that the most critical identity functions are reserved to the primary key and cannot be delegated to a secondary key at all — see **Primary Key Exclusive Functions** below.
:::
### Updating Permissions
The primary key can modify permissions for any secondary key at any time:
- Add or remove specific permissions using `identity::set_secondary_key_permissions`
- Completely revoke access by removing the secondary key using `identity::remove_secondary_keys`
- Temporarily disable all or re-enable secondary keys by freezing them using `identity::freeze_secondary_keys` and `identity::unfreeze_secondary_keys`
### Limitations and Considerations
- A single key can only be associated with one identity (either as primary or secondary key)
- Secondary keys cannot add other secondary keys directly without explicit `add_authorization` permission
- There's a limit to the complexity and number of permissions that can be assigned
- Secondary keys can voluntarily leave an identity but cannot modify their own permissions
- Frozen secondary keys cannot perform any operations until unfrozen by the primary key
**Primary Key Exclusive Functions:**
The following critical identity management functions can **only** be performed by the primary key:
- Setting or modifying permissions of secondary keys (`set_secondary_key_permissions`)
- Adding/removing secondary keys (`add_secondary_keys_with_authorization`, `remove_secondary_keys`)
- Freezing/unfreezing secondary keys (`freeze_secondary_keys`, `unfreeze_secondary_keys`)
- Multisig administration (when designated as admin or payer identity)
**Secondary Key Authorization Capabilities:**
Secondary keys with permission to call `add_authorization` can create authorization requests for sensitive operations, including:
- Primary key rotation requests
- Asset ownership transfers
- Portfolio custody changes
- Other identity-level permissions
:::warning Critical Security Consideration
Granting a secondary key permission to call `add_authorization` effectively allows it to initiate **primary key rotation** and other sensitive identity operations. While the target must still accept these authorizations, this permission should be granted with extreme caution as it can serve as a backup mechanism to regain identity control if the primary key is lost.
**Recommendation**: Only grant `add_authorization` permissions to highly trusted secondary keys that you intend to use as emergency backup keys for identity recovery scenarios.
:::
**Key Recovery Implications:**
- If a secondary key leaves or is removed from an identity, it cannot interact with asset or identity related transactions on the Polymesh blockchain until it receives its own DID through onboarding or joins another identity as a secondary key.
These restrictions are enforced at the protocol level, ensuring that control over core identity structure remains secure while allowing controlled delegation of specific capabilities.
### Transaction Fee Management
A key consideration when implementing secondary keys is managing transaction fees:
- Each secondary key requires its own POLYX balance to pay for transaction fees
- POLYX balances are key-specific and independent - primary keys cannot access or use the POLYX balance of secondary keys and vice versa
- Alternative to maintaining individual balances, [subsidized accounts](/accounts/subsidized) can be set up to pay for transaction fees on behalf of specified secondary keys
When implementing a multi-key strategy for your identity, make sure to account for POLYX distribution to ensure all keys have sufficient balance to execute their assigned functions, whether through individual balances or subsidization.
## Advanced Topics
### Smart Contracts and Multisigs
[Smart contracts](/development/smart-contracts) and [Multisigs](/identity/advanced/multisig) can also be attached to identities as secondary keys in Polymesh, providing powerful functionality for implementing strict controls or automated business logic while maintaining proper identity management.
### Secondary Keys and Separate DIDs
When managing complex organizational or operational structures on Polymesh, you may need to decide between delegating access via secondary keys within the same identity, or creating a fully independent identity for a separate key through DID registration.
| Secondary Keys | Separate DIDs |
| ------------------------------------ | ---------------------------------- |
| Share DID with primary key | Each key has its own DID |
| Share compliance status | Independent compliance status |
| Limited by permissions | Full identity capabilities |
| Cannot have their own secondary keys | Can have their own secondary keys |
| Best for delegating tasks | Best for organizational separation |
## Best Practices
1. **Primary Key Security**
- Store primary key securely and minimize its use
- Use properly permissioned secondary keys for routine operations
- Implement clear key rotation policies
2. **Permission Management**
- Follow least-privilege principle when assigning permissions
- Document all key permissions and their intended use
- Regularly audit and update permissions as needed
3. **Operational Controls**
- Create clear policies for key usage and sharing
- Implement role-based access through appropriate permissions
- Maintain documentation of all key management procedures
4. **Security Measures**
- Conduct regular security audits of all keys and permissions
- Remove unused or unnecessary keys promptly
- Plan for key rotation and recovery scenarios
## Conclusion
Secondary keys provide a powerful way to implement secure and flexible identity management on Polymesh. When properly implemented with appropriate permissions and controls, they enable organizations to maintain strong security while supporting efficient operations. Understanding the relationship between secondary keys and smart contracts allows for creating sophisticated and secure organizational structures on the blockchain.
---
## Multisig Walkthrough
For detailed steps on creating and managing multisigs, follow this walkthrough. These steps are shown using the https://testnet-app.polymesh.live UI but can also be performed on https://mainnet-app.polymesh.network
## Creating a MultiSig
In Polymesh, MultiSigs are always created by an existing identity and are attached to that identity as a secondary key during creation.
:::note
The **Paying Identity** of the newly created MultiSig will be set to the creator's identity.
:::
### Step 1 - Setting Up Accounts (Signing Keys)
In this tutorial, we will create a 2 of 3 MultiSig and set it as the primary key of our Polymesh identity.
To begin, we create three new keys, called `TEST_SIGNER_1/2/3`. In practice, these keys can be created and secured by three different parties.

These keys can be created in any [wallet](/getting-started/keys) compatible with Polymesh or using the Polymesh App (https://mainnet-app.polymesh.network) in the Accounts tab. We've used the latter for this tutorial.
### Step 2 - Creating A MultiSig
We now create the MultiSig key by calling the `multisig::createMultisig` extrinsic. This can be submitted by the identity's primary key, or by a secondary key that is permissioned to call it — but only the **primary key** can attach custom permissions to the resulting MultiSig signer key. A secondary-key caller must omit permissions (passing custom permissions is rejected with `KeyNotAllowed`), so the MultiSig signer key is created with empty permissions.
In this tutorial, `ALICE_STASH` is the Primary Key of our identity (`0x01`), so we use this key to submit the `multisig::createMultisig` transaction.
This requires us to pass in the initial set of signers for the MultiSig and specify how many of those signers are required to agree in order to execute a transaction. We specify the three new keys created in Step 1 here.

Once this extrinsic has been executed, the MultiSig is created and added to the creator's identity as a secondary key. We can check for the corresponding event, which will tell us the address of the newly created MultiSig key.

In this case, the MultiSig has an address of `5CLybi9gPdE68Ls6PDkz1KnmVH6UZcQLa7Wmt7WgEWTUFoo6`. You can add this to your address book in the Polymesh App to make it easier to reference in the future.

### Step 3 - Accepting Signer Authorizations
When we created the MultiSig, the signers are not immediately linked to it. You can see in the events emitted that 3 instances of the `AuthorizationAdded` event were emitted. Authorization requests were created for the signers to join the MultiSig, and they must accept the requests to confirm their desire to be linked to the MultiSig. We now need to accept the authorization from each of the three signers specified in Step 2.
To accept these authorizations, we need the authorization ID for each signer. These are available from the events emitted when the MultiSig is created, or alternatively, you can query the `identity::authorizations` storage or view pending authorization requests in the [Polymesh Portal](https://portal.polymesh.network/authorizations).

Once you have the authorization ID for each signing key, you can call `multiSig::acceptMultisigSignerAsKey` separately from each of the signing keys (each signer will have a different authorization ID to accept).

:::info
When accepting these authorizations, the current primary key of the _Paying Identity_ will pay for the transactions.
:::
At this point, your MultiSig is created, and the three signing keys have been associated with it.
### Step 4 - Funding The MultiSig
To fund your MultiSig, you can transfer POLYX to it like any other key.

If you added the MultiSig to your Address Book, you should now see its updated balance.

## Using The MultiSig
You can now use your MultiSig to execute actions as your identity. For example, suppose we want to create a new asset; we can call `asset::createAsset`.
Before executing this transaction from our MultiSig, the MultiSig must be permissioned to call the `asset::createAsset` extrinsic as a secondary key. Unless explicitly specified during creation, the MultiSig is assigned no permissions by default.

To create an asset, you need to first create the transaction from one signer, and then approve it from another signer (since 2 of the 3 signers must agree to execute an action).

When the first signer proposes the transaction, the corresponding event shows the proposal ID, which can be used by the second signer to approve the transaction.

In this case, the proposal ID is 0, which we then approve from the second signer.

You can now see that the `asset::createAsset` transaction is executed!

## Adding Admin Identity
It is possible to assign an _Admin Identity_ to your MultiSig. The Primary Key of the _Admin Identity_ can then add/remove signers and update the number of signers a proposal requires in order to execute.
Once added, an _Admin Identity_ can be removed either by the MultiSig itself or the Primary Key of the _Admin Identity_.
To administer the MultiSig, the Primary Key of the _Admin Identity_ must submit the relevant transactions.
Assigning an _Admin Identity_ can be done through a usual MultiSig proposal to `multisig::addAdmin`, which is then approved in the usual way.


The event confirming the new _Admin Identity_ can then be observed.

Once the _Admin Identity_ has been approved, it can, for example, update the number of approvers needed from 2 to 1.


## Removing Payer Identity
On creation, a MultiSig is assigned the identity of the calling key as its _Paying Identity_.
You may wish to remove this association, leaving the MultiSig to pay for its own transactions from its POLYX balance (rather than relying on the primary key of the _Paying Identity_ to pay for its associated transactions).
A _Paying Identity_ can be removed either by the MultiSig or by the _Paying Identity_ itself. In this example, we do the former.


## Changing Identity
It may be necessary for the MultiSig to detach from its current identity and attach to a new identity.


To join a new identity, the new identity must issue a `JoinIdentity` authorization to the MultiSig.


The MultiSig can then accept this `JoinIdentity` authorization. Note that when accepting a `JoinIdentity` authorization, it is always the primary key of the identity that issued the `JoinIdentity` authorization that pays for the transaction fee to accept the authorization.
To join a new identity, a MultiSig must use the dedicated extrinsic `multisig::approveJoinIdentity` - this ensures that payment is routed correctly (otherwise the transaction cannot complete, as, in this instance, the MultiSig cannot pay for itself as it is not yet attached to an identity).


Note that it is also possible to simplify the workflow using Polymesh's native batching functionality.
Instead, you can create a proposal for an atomic batch of two transactions using `utility::batch`:
- Leave the current identity by executing `identity::leaveIdentityAsKey`
- Join the new identity by executing `identity::joinIdentityAsKey`
The latter approach is recommended provided the `JoinIdentity` authorization can be created before the MultiSig needs to leave its current identity.
---
## Multisigs
## Overview
A multisig in Polymesh is a specialized account that requires approval from multiple parties to execute transactions. Instead of using a single private key, a multisig allows you to specify a group of signing accounts (m signers) and define how many of those signers (n signatures) must agree before any transaction can be executed. On Polymesh, multisig keys behave the same as other key types, i.e., they can be associated with an identity as either a primary or secondary key with defined permissions.
Multisigs enhance security and governance by distributing control across multiple parties while maintaining a single point of identity on the blockchain.
## Core Concepts
### Structure and Roles
A multisig consists of several key components:
- **Signing Keys**: The accounts authorized to approve transactions (m signers)
- **Required Signatures**: The minimum number of approvals needed (n signatures)
- **Admin Identity**: Optional identity that can manage multisig configuration
- **Paying Identity**: The creator's identity, which pays for proposal transaction fees (can be removed)
- **Multisig Identity**: The identity to which the multisig is linked
### Key Relationships
Multisigs have three important relationships:
1. **Identity Association**: Like any key, a multisig must be linked to an identity to perform most operations
2. **Admin Control**: An optional admin identity can manage the multisig configuration
3. **Fee Payment**: Initially the creator's identity pays for proposal fees, but this can be removed to make the multisig self-paying. Multisig accounts can also be [subsidized](/accounts/subsidized).
### Operational Flow
The basic flow for multisig operations:
1. A signer proposes a transaction
2. Other signers review and approve/reject the proposal
3. When sufficient approvals are gathered, the transaction executes automatically
4. If rejected or executed, the proposal is removed from chain storage
## Managing a Multisig
### Creation and Setup
When a multisig is created by calling `multisig::create_multisig`:
1. It's automatically added as a secondary key to the creator's identity. Optionally the creator can assign [secondary key permissions](/identity/advanced/secondary-keys/#secondary-key-permissions) to the multisig when creating — but only if the caller is the **primary key** of the creator's identity. A secondary key can also call `create_multisig`, but only without specifying custom permissions (the new multisig signer key is then created with empty permissions); passing custom permissions from a secondary key is rejected with `KeyNotAllowed`.
2. The creator's identity becomes the paying identity
3. Authorization requests are sent to all designated signers
4. Signers must accept their authorizations before participating
When selecting signing keys for a multisig consider the following requirements:
- Signing keys cannot be linked to other identities or multisigs
### Administration
A multisig can optionally have an admin identity that can unilaterally manage its configuration. An admin identity can be added by creating a multisig proposal with a call of `multisig::add_admin`. The admin identity's **primary key** can then:
- Add a signer to the multisig (`multisig::add_multisig_signers_via_admin`)
- Remove a signer from the multisig (`multisig::remove_multisig_signers_via_admin`)
- Change the required number of signatures (`multisig::change_sigs_required_via_admin`)
:::warning Primary Key Requirement
Multisig administration functions require the **primary key** of the admin identity - these cannot be delegated to secondary keys. This ensures strict control over multisig configuration changes.
:::
:::note
See [Proposal Voting](#proposal-states) for details on how configuration changes affect existing proposals.
:::
### Configuration changes
If no admin identity is set (or as an alternative to admin-based changes), a multisig can modify its configuration through its normal [proposal process](#multisig-proposals).
The following configuration changes are possible:
#### Adding/Removing Signers
- Add new signers via `multisig::add_multisig_signers`
- Remove existing signers via `multisig::remove_multisig_signers`
:::note
You cannot remove signers if it would cause the total number of signers to fall below the required number of approvals
:::
#### Changing Required Signatures
- Modify the number of required signatures via `multisig::change_sigs_required`
- The new required signatures value must be:
- Greater than 0
- Less than or equal to the total number of signers
### Payment Management
Transaction fees for multisig proposals are handled in one of two ways:
1. **Paying Identity**: Initially set to the creator's identity, this identity's primary key covers transaction fees for all multisig proposals
2. **Self-Paying**: The multisig can be configured to pay its own fees by removing the paying identity
In either case, the paying key must have sufficient POLYX to submit and execute the transaction.
Important points about the paying identity:
- It is automatically set to the creator's identity when the multisig is created
- It can be removed either by:
- The multisig itself using `multisig::remove_payer`
- The **paying identity's primary key** using `multisig::remove_payer_via_payer`
- It cannot be changed to a different identity
- Once removed, the multisig must pay its own transaction fees from its POLYX balance
- The removal is permanent - after removing the paying identity, the multisig cannot revert to using a paying identity
:::warning Primary Key Requirement
The `multisig::remove_payer_via_payer` function requires the **primary key** of the paying identity - this cannot be delegated to secondary keys.
:::
:::tip
Once a paying identity has been removed, if another key is required to pay transaction fees, a [account subsidy](/accounts/subsidized) relationship can be set up.
:::
### Joining a New Identity
When a multisig needs to move from one identity to another, there is a specific process that must be followed:
1. The multisig must first leave its current identity using `identity::leave_identity_as_key` as a proposal or be removed by the identities primary key.
2. The new identity must issue then issue a `JoinIdentity` authorization to the multisig
3. The multisig must accept this authorization using the specific extrinsic `multisig::approve_join_identity`
:::important
Multisigs must use `multisig::approve_join_identity` instead of the standard `identity::join_identity_as_key` when accepting a join identity authorization. This ensures transaction fees are handled correctly, as a multisig that is not attached to an identity cannot pay for its own transactions.
:::
This process can be optimized by using Polymesh's batching functionality to combine leaving the current identity and joining the new identity into a single atomic transaction using `utility::batch`. This approach is recommended when the `JoinIdentity` authorization request can be obtained before the multisig needs to leave its current identity.
## Multisig Proposals
### Proposal Creation
To create a multisig proposal, a signer must call the `multisig::create_proposal` extrinsic. This allows the proposer to wrap a standard blockchain call into a proposal that other multisig signers can vote on. The creator must be one of the authorized signers of the multisig, and by creating the proposal they automatically cast an approval vote.
When creating a proposal, you can specify:
- The transaction to be executed (wrapped as a proposal)
- An optional expiry after which the proposal will expire if not executed
- Arguments specific to the transaction being proposed
Once created, proposals cannot be modified - they can only be approved or rejected by other signers. If enough signer threshold is reached during creation, the proposal will execute right away without requiring additional approvals.
:::tip
When using the Polymesh SDK, regular transactions from a multisig signer must use `.runAsProposal()` instead of `.run()`. The SDK will automatically wrap the transaction with `multisig::create_proposal`. Proposal approvals/rejections by signers should still use `.run()` directly.
:::
### Proposal Voting
Signers vote on proposals by calling either `multisig::approve` or `multisig::reject`, providing the multisig address and the proposal ID generated during the proposal creation as arguments. These transactions must be submitted directly by the signer's key, not through another multisig proposal.
Key voting aspects:
- The proposal creator's vote is automatically set to approve upon creation
- Other signers can vote exactly once on each proposal
- The proposal creator has special privileges:
- Can change their vote to reject if no other signers have voted yet
- Changing to reject in this case automatically rejects the proposal
- The proposal executes immediately when the required number of approvals is reached
- A proposal is rejected when rejection votes exceed total signers minus required approvals
### Proposal Management
Proposals have several states and features:
- Can be created by any authorized signer
- Remain active until executed, rejected, expired or invalidated
- May have optional expiration times
#### Proposal States
A proposal can be in one of the following states:
- **Active**: Currently collecting approvals, not yet executed
- **Expired**: Passed its expiration time without sufficient approvals
- **Failed**: Execution was complete but the transaction failed
- **Invalid**: No longer valid due to configuration changes
- **Successful**: Successfully executed
:::note Invalid Proposals
The following situations will cause proposals to be invalidated.
- Configuration changes (modifying signers or required signatures) invalidate all pending proposals
- Removal of a multisig account from an identity
- A chain upgrade containing breaking transaction changes
Invalidated proposals must be recreated as new proposals if required.
:::
:::note
Historical proposal details can be retrieved via the [SubQuery](/development/tooling/subquery) chain indexing service or by querying historical blocks on an archive node.
:::
---
## Authorizations
## Overview
Authorizations in Polymesh provide a secure framework for managing permissions and access between identities and keys. They enable controlled delegation of permissions while maintaining security through explicit consent from all parties involved.
## How It Works
The authorization process follows a simple request-approval flow:
1. An identity (the authorizer) creates an authorization request for a specific action
2. The target identity or key (the authorized) receives the pending authorization
3. The authorized party can view, accept, or reject the authorization
4. Upon acceptance, the authorized action is executed with proper permissions
:::note
Authorization requests can optionally include an expiry time. If specified and not accepted within that timeframe, they become invalid and must be reissued.
:::
## Common Use Cases
The authorizations framework on Polymesh facilitates various identity management and permission scenarios including:
- **Identity Key Management**: Adding secondary keys or rotating primary keys
- **Asset Control**: Transferring ownership of assets or tickers between identities
- **Custody Management**: Granting portfolio custody/control rights to other identities
- **Agent Permissions**: Authorizing entities to act as agents for specific assets
- **Fee Management**: Setting up subsidized account relationships
- **MultiSig Operations**: Adding signers to multisig arrangements
## Authorization Types
Polymesh supports several types of authorizations, each serving different permission management needs:
### Identity Management
- **Join Identity**: Authorize a key to become a [secondary key](/identity/advanced/secondary-keys) with specified permissions
- **Rotate Primary Key**: Allow changing of an identity's primary key
- **Rotate Primary to Secondary**: Convert the current primary key into a secondary key during rotation
### Asset Control
- **Transfer Asset**: Authorize transfer of asset ownership between identities
- **Transfer Ticker**: Allow transfer of ticker ownership or reservation rights
- **Become Agent**: Grant permission to act as an [agent](/asset-agents) for specific asset operations while defining specific actions the agent can perform
### Operational Control
- **Portfolio Custody**: Enable third part [portfolio custody](/portfolios/custody) management or control of specific portfolios while maintaining beneficial ownership under the original onchain identity
- **Add MultiSig Signer**: Authorize addition of new signers to a [multisig](/identity/advanced/multisig) arrangement.
## Managing Authorizations
### Creating Requests
Authorization requests can be created using `identity::add_authorization`. Each request must specify:
- The target identity or key
- The type of authorization
- Any required authorization data (e.g., permissions for secondary keys)
- Optional expiry time
:::warning Authorization Security Considerations
The `add_authorization` function can be called by **secondary keys** with appropriate permissions, not just primary keys. This means a secondary key with permission to call `add_authorization` can create sensitive authorization requests including:
- **Primary key rotation requests** - Potentially allowing identity recovery if the primary key is lost
- **Asset ownership transfers** - Initiating transfer of asset control
- **Portfolio custody changes** - Requesting changes to portfolio management rights
**Security Recommendation**: Only grant `add_authorization` permissions to highly trusted secondary keys. While targets must still accept these authorizations, the ability to initiate such requests should be carefully controlled as it can serve as a powerful backup mechanism for identity management.
`add_authorization` is one instance of a broader principle: extrinsic permissions delegate the full authority of the calls they allow, so permission to call any permission-, key-, or authorization-management extrinsic effectively delegates that management authority. See [Extrinsic Permissions Delegate the Authority of the Calls They Allow](/identity/advanced/secondary-keys#extrinsic-permissions-delegate-the-authority-of-the-calls-they-allow).
:::
In some special cases, a dedicated method may be provided to create an authorization request. For example, `multisig::add_multisig_signers_via_admin` allows a permissioned admin identity to act on behalf of the multisig to add a new signer.
Some operations also support off-chain authorization signatures as an alternative to the request-approval flow. This includes adding [secondary keys](/identity/advanced/secondary-keys/#2-off-chain-authorization). These methods typically require the target key to sign specific authorization data off-chain, with the payload data wrapped in `` and `` tags before signing, enabling operations to complete in a single transaction.
Each new authorization request is assigned a unique authorization ID which is used to query details of the authorization request and accept or reject it.
### Processing Requests
Targets can manage authorization requests through:
- **Accepting**: Approve and execute the authorized action through an authorization-specific method by providing the authorization ID
- **Rejecting**: Cancel pending authorization requests (can be done by either the authorizer or target) by calling `identity::remove_authorization`
- **Review Pending Incoming Requests**: View pending authorizations targeting the identity or key using `identity::Authorizations`
- **Review Pending Issued Requests**: Query past authorizations using `identity::AuthorizationsGiven`
:::warning
Authorization management is critical for identity security. Always verify authorization types and permissions before acceptance.
:::
### Fee Management
For some authorization types, the accepting identity may not be able to hold POLYX and hence cannot pay the transaction fee to accept the authorization request. In these cases, the primary key of the identity that initiated the authorization request (the authorizer) pays the transaction fee when the authorization is accepted by the target.
The following acceptance methods have their transaction fees paid by the authorizer's primary key:
- `identity::join_identity_as_key`
- `identity::rotate_primary_key_to_secondary`
- `identity::accept_primary_key`
- `multisig::accept_multisig_signer`
For all other authorization acceptance methods, the transaction fee is paid by the accepting party's key.
:::note
Subsidy setup no longer goes through the authorization system: `relayer::approve_subsidy` and `relayer::accept_subsidy` manage the relationship directly, without an authorization ID. See [Subsidized Accounts](/accounts/subsidized).
:::
When removing/rejecting an authorization request, the caller can optionally specify that the primary key of the requestor's identity pays the transaction fee by setting the `auth_issuer_pays` parameter to `true`.
### Monitoring Authorizations
Identities can monitor their authorizations through several methods:
- **Pending Incoming Requests**: View pending authorizations targeting the identity or key using `identity::Authorizations`
- **Pending Issued Requests**: Query past authorizations using `identity::AuthorizationsGiven`
:::info
Authorization requests that have been consumed, rejected, expired, or revoked cannot be reused. A new authorization request must be created if needed.
:::
---
## Subsidized Accounts - Fee Relayer
## Overview
Subsidized accounts in Polymesh allow one account key (the subsidizer) to pay both network transaction fees and protocol fees on behalf of another account key (the subsidized). This feature enables businesses to cover transaction costs for their users, removing friction from the user experience while maintaining proper identity attribution for regulatory compliance.
## Use Cases
Common scenarios for using subsidized accounts include:
- **DApp Providers**: Covering transaction fees for their users to improve user experience
- **Asset Issuers**: Subsidizing operations for their investors or service providers
- **Service Providers**: Offering fee coverage as part of their service package
- **Corporate Entities**: Managing internal operations where a central account pays for multiple department activities
- **Institutional Users**: Organizations that prefer not to hold POLYX directly on their balance sheets can engage third-party service providers to manage token holdings and handle transaction fee payments
:::note
The subsidized accounts feature is particularly valuable for institutions with regulatory or accounting considerations regarding utility token holdings. By using a third-party subsidizer to handle transaction fees, organizations can participate in the network while outsourcing the management of POLYX tokens.
:::
## How It Works
The subsidization process involves two main roles:
1. **Subsidizer**: The account key that pays for transaction fees
2. **Subsidized Account**: The account key whose transactions are being paid for
The process follows these steps:
1. A subsidizer account sets up an allowance for a target account
2. When the subsidized account submits a supported transaction, the chains relayer functionality automatically:
- Validates the transaction type is eligible for subsidization
- Checks the available allowance
- Deducts fees from the subsidizer's account instead of the sender
:::note
Subsidies are established at the account level. All transactions from the subsidized account can benefit from the subsidy for eligible transactions.
:::
## Limitations and Considerations
Before implementing subsidized accounts, be aware of these important limitations:
1. **Account Restrictions**:
- When an account has an active subsidy, it cannot submit non-subsidized transactions
- Only specific transactions, defined in the chain runtime are eligible for subsidization. All transactions related to Asset and Identity management are supported.
- This restriction helps prevent fee circumvention and ensures proper subsidy tracking
2. **Batch Transaction Restrictions**:
- When using subsidies, batch transactions are limited to a maximum of 7 transactions
- Nested batch calls are not supported when using subsidies
3. **Allowance Management**:
- Subsidies have a maximum allowance that cannot be exceeded
- Once an allowance is exhausted, it must be renewed by the subsidizer before the key can transact again
4. **Transaction Requirements**:
- The subsidizer must have sufficient balance to cover the fees
- Failed transactions still consume the allowance for the transaction fees consumed
## Technical Implementation
### Setting Up Subsidies
Subsidizers establish a subsidy by calling `relayer::approve_subsidy`, providing the account key to be subsidized and the maximum POLYX allowance for the subsidy. This writes the pending subsidy directly to chain state — unlike most delegated permissions on Polymesh, it does **not** go through the [authorization](/authorizations) system, so there is no authorization ID involved. The subsidized key then calls `relayer::accept_subsidy`, naming the paying key, to establish the relationship.
Before the subsidized key accepts, the subsidizer can cancel the pending offer by calling `relayer::revoke_subsidy`.
### Managing Subsidies
Both the subsidizer and subsidized party have control over the subsidy relationship:
**Subsidizer Controls**:
- `relayer::decrease_polyx_limit`: Reduce the subsidy allowance
- `relayer::increase_polyx_limit`: Increase the subsidy allowance
- `relayer::update_polyx_limit`: Set the subsidy allowance to a specific value
- `relayer::revoke_subsidy`: Cancel a pending, not-yet-accepted subsidy offer
- `relayer::remove_subsidy`: Remove an established subsidy relationship
**Subsidized Party Controls**:
- `relayer::remove_subsidy`: The subsidized key can unilaterally exit the subsidy relationship at any time
:::important
The ability for the subsidized key to unilaterally exit the relationship is an important protection mechanism. It ensures that the subsidizer cannot censor transactions by maintaining control over the relationship - the subsidized party can always choose to exit the subsidy arrangement and transact normally.
:::
### Monitoring Subsidies
Participants can monitor subsidy status using chain state queries `relayer::subsidies` to view the subsidy relationship and remaining allowance.
### Transaction Fee Behavior
When a transaction is subsidized:
1. The chain verifies the transaction type is eligible for subsidization
2. The relayer pallet automatically handles fee payment from the subsidizer's account
3. Both network fees and protocol fees are covered by the subsidy
4. The transaction maintains its original identity attribution for compliance purposes
## Best Practices
When implementing subsidized accounts:
- Set reasonable allowance limits
- Monitor usage patterns
- Monitor allowance levels to prevent disruption
- Regularly review subsidy relationships
- Plan for allowance renewals
:::info
Subsidized accounts provide a powerful way to manage transaction fees while maintaining proper identity attribution. The automatic handling by the Relayer pallet makes it seamless for end users while ensuring proper controls and tracking are maintained.
:::
---
## Advanced Identity
# Advanced Identity Features
Identity in Polymesh extends beyond basic concepts with powerful features designed for enterprise-grade security, flexibility, and control. This section covers advanced identity mechanisms that enable sophisticated access control, delegation, and organizational structures.
## Key Topics
- **[Secondary Keys](/identity/advanced/secondary-keys)** - Delegate specific permissions to different keys while maintaining granular control
- **[Multisigs](/identity/advanced/multisig)** - Create multi-signature structures requiring approval from multiple parties
- **[Authorizations](/authorizations)** - Manage permission requests between identities and keys
- **[Subsidized Accounts](/accounts/subsidized)** - Configure accounts where one entity pays transaction fees for another
These features work together to provide comprehensive identity management capabilities for institutional use cases while maintaining regulatory compliance.
---
## Identity
## Overview
Identity is central to Polymesh. Network participants must have an on-chain identity to interact with **asset** and **identity** related transactions on the blockchain.
Each identity:
- Is created through DID onboarding and registration (self-registration or registrar-assisted)
- Is referenced by a pseudo-anonymous decentralized identifier (DID), e.g., `0xfc0d2fc058d02c0a89c2cc2ff11726971dd39886a0b80ecfaa80fa3f196d65ce`
- Can hold [asset](/core/assets) balances via [portfolios or Account IDs](/asset-holdings) connected to the identity.
- Can be associated with [claims](/compliance/#claims) and have [permissioned roles](/identity/roles)
- Is controlled by a [primary key](/identity/advanced/primary-keys) and optional [secondary keys](/identity/advanced/secondary-keys)
## Identity Requirement on Polymesh
An identity is required for:
- All identity-related transactions (such as key management, adding or removing keys, and permissioned roles)
- All asset-related transactions (such as holding, minting, and distributing assets)
- Node operators
- Permissioned roles (such as committee or council members)
An identity is not required for:
- POLYX token operations, such as transfers between accounts
- Staking operations for nominators with stash and controller accounts (such as bonding, nominating, and unbonding)
## Key Concepts
### Identity Creation & Verification
Identities are created through DID registration. Users can self-register using `identity::self_register_did`, or a permissioned DID registrar can register a DID for a target key using `identity::register_did`.
### Asset Management
Native [assets](/core/assets) (excluding [POLYX](/polyx)) held by users are associated with their on-chain identity, via [portfolios or directly on a signing key's account](/asset-holdings), provided that key is associated with an identity as either a [primary key](/identity/advanced/primary-keys) or [secondary key](/identity/advanced/secondary-keys). Each identity can hold multiple assets and manage them through different portfolios. Identities may also be granted [agent permissions](/asset-agents) to perform specific operations on behalf of asset issuers.
### Claims & Compliance
Identities can receive [claims](/compliance#claims) from other identities, which are used to enforce on-chain [compliance rules](/compliance) for assets. Claims have specific [scopes](/compliance#claim-scopes) and can be used to represent various attributes, such as KYC status or accreditation.
### Key Management
Each identity has:
- A single [primary key](/identity/advanced/secondary-keys#relationship-to-primary-key) with full control
- Optional [secondary keys](/identity/advanced/secondary-keys) with configurable permissions
- Support for [multisig](/identity/advanced/multisig) keys as either primary or secondary keys
- Support for [smart contracts](/development/smart-contracts/) as either primary or secondary keys
Keys can also enter into a [subsidized](/accounts/subsidized/) relationship with another key, allowing the subsidizer to pay transaction fees on its behalf.
### Access Control
Polymesh provides robust [authorization](/authorizations) and [permission](/identity/advanced/secondary-keys#secondary-key-permissions) frameworks to manage access between identities and their keys.
## Identity Diagrams
The following diagrams help visualize and clarify the relationships between key aspects of on-chain identities in Polymesh. Each diagram focuses on a specific concept, such as DID onboarding, [primary and secondary keys](/identity/advanced/secondary-keys), [portfolios](/portfolios), [custody](/portfolios/custody), [claims and compliance](/compliance), [agent permissions](/asset-agents), [subsidized accounts](/accounts/subsidized), and [secondary key permissions](/identity/advanced/secondary-keys#secondary-key-permissions). For more details, see the linked documentation pages for each topic.
### Identity Onboarding
This diagram shows DID onboarding through self-registration or a permissioned DID registrar.
```mermaid
flowchart TD
User["User"]
UserKey["Primary Signing Key"]
SelfReg["Self-register DID identity::self_register_did"]
Registrar["Permissioned DID Registrar registers DID for user"]
MainDID["On-chain Identity (DID)"]
User --> UserKey
UserKey --> SelfReg
UserKey --> Registrar
SelfReg --> MainDID
Registrar --> MainDID
```
---
### Keys & Accounts
This diagram shows how an identity (DID) is controlled by signing keys. Both the [primary key](/identity/advanced/primary-keys) and any number of optional [secondary keys](/identity/advanced/secondary-keys) are grouped in a "Signing Keys" subgraph. Each key can be a standard signing key (SR25519, ED25519, or ECDSA), a [multisig](/identity/advanced/multisig), or a [smart contract](/development/smart-contracts/).
```mermaid
flowchart TD
MainDID["On-chain Identity (DID)"]
POLYX["POLYX Balance"]
subgraph SG1 ["Signing Keys"]
direction TB
style SG1 stroke-dasharray: 4
PrimKey["Primary Key"]
SecKey["Secondary Key(s) (optional)"]
end
MainDID --> SG1
POLYX --> SG1
subgraph SG2 ["Allowed Key Types"]
direction TB
style SG2 stroke-dasharray: 4
StdKey["Standard Signing Key (SR25519/ED25519/ECDSA)"]
MultiSig["Multisig (m of n)"]
SmartContract["Smart Contract"]
end
PrimKey & SecKey --> SG2
MultiSig --> Signer1["Standard Signing Key (SR25519/ED25519/ECDSA) (No associated with a DID)"]
```
---
### Secondary Key Permissions
Secondary keys can be granted fine-grained [permissions](/identity/advanced/secondary-keys#secondary-key-permissions), allowing them to perform only specific actions or access certain resources. Permissions can be scoped to:
- **Transactions**: Restrict which blockchain functions the key can call (e.g., transfers, settlements, etc.).
- **Portfolios**: Restrict which portfolios the key can access or manage — this is what governs the key's ability to move and transfer assets not directly held by their key.
- **Assets**: Restrict which assets the key can administer **as an external agent** (issuance, compliance, and other agent actions). Asset permissions do **not** restrict ordinary transfers of an asset out of a portfolio — that is governed by portfolio permissions. See [Asset permissions do not restrict transfers](/identity/advanced/secondary-keys#secondary-key-permissions).
The diagram below illustrates how secondary key permissions can be configured:
```mermaid
flowchart TD
SecKey["Secondary Key"]
Permissions["Permissions"]
SecKey --> Permissions
Permissions --> AssetPerms["Asset Permissions"]
Permissions --> PortfolioPerms["Portfolio Permissions"]
Permissions --> TxPerms["Transaction Permissions"]
AssetPerms --> Asset1["Asset A"]
AssetPerms --> Asset2["Asset B"]
PortfolioPerms --> Portfolio1["Portfolio 1"]
PortfolioPerms --> Portfolio2["Portfolio 2"]
TxPerms --> Transfer["Settlement"]
TxPerms --> Settle["Asset"]
TxPerms --> OtherTx["Other Transaction Types"]
```
---
### Portfolios & Asset Management
This diagram demonstrates how an identity can have multiple [portfolios](/portfolios) (default, user, custodial) to organize and manage [assets](/core/assets), including both fungible and non-fungible tokens.
```mermaid
flowchart TD
MainDID["On-chain Identity (DID)"]
DefPortfolio["Default Portfolio"]
UserPortfolio["User Portfolio"]
CustodialPortfolio["Custodial Portfolio"]
AddUserPortfolios["Additional User Portfolios"]
AssetA["Asset A"]
AssetB["Asset B"]
AssetC["Asset C"]
MainDID --> DefPortfolio
MainDID --> UserPortfolio
MainDID --> AddUserPortfolios
MainDID --> CustodialPortfolio
DefPortfolio --> AssetA
UserPortfolio --> AssetB
CustodialPortfolio --> AssetC
AddUserPortfolios --> AssetB
AddUserPortfolios --> AssetC
```
---
### Custody & Portfolio Control
This diagram shows how portfolio [control/custody](/portfolios/custody) can be assigned to another identity (the custodian), who then controls the portfolio's assets.
```mermaid
flowchart TD
OwnerID["Owner Identity"]
CustodialPortfolio["Custodial Portfolio"]
CustodianID["Custodian Identity"]
CustodianKeys["Custodian Signing Keys"]
AssetA["Asset A"]
AssetB["Asset B"]
OwnerID -->|"Owner retains beneficial ownership but cannot transfer"| CustodialPortfolio
CustodialPortfolio --> AssetA
CustodialPortfolio --> AssetB
CustodianID -->|"Custodian has exclusive control of assets"| CustodialPortfolio
CustodianKeys --> CustodianID
```
---
### Claims & Compliance
This diagram illustrates how [claims](/compliance#claims) (such as KYC or accreditation) are issued to an identity by claim issuers, and how they are used for [compliance](/compliance) purposes. Multiple claims from different issuers can be attached to a single identity and may be scoped to specific assets, identities or custom identifiers.
```mermaid
flowchart TD
MainDID["On-chain Identity (DID)"]
Claim1["KYC Claim Asset A"]
Claim2["Accreditation Claim Asset A"]
Claim3["KYC Claim Asset B"]
Claim4["Jurisdiction Claim Asset B"]
ClaimIssuer["Claim Issuer 1"]
ClaimIssuer2["Claim Issuer 2"]
ClaimIssuer -->|"Issues"| Claim1
ClaimIssuer -->|"Issues"| Claim2
ClaimIssuer2 -->|"Issues"| Claim3
ClaimIssuer2 -->|"Issues"| Claim4
Claim1 --> MainDID
Claim2 --> MainDID
Claim3 --> MainDID
Claim4 --> MainDID
```
---
### Agent Permissions
This diagram shows how an identity can grant [agent permissions](/asset-agents) to another identity, allowing the agent to act on behalf of the original identity for specific assets or operations. The asset issuer is an agent for its own asset by default.
```mermaid
flowchart TD
MainDID["On-chain Identity (DID)"]
AgentID["Agent Identity"]
Asset["Asset"]
MainDID -->|"Owns and controls (Issuer is an agent by default)"| Asset
MainDID -->|"Grants agent permission"| AgentID
AgentID -->|"Acts on behalf of issuer"| Asset
```
---
### Relayer & Subsidized Keys
This diagram shows how a [relayer](/accounts/subsidized) can subsidize transaction fees for a signing key (not directly for an identity), allowing another key (owned by a relayer identity) to pay fees on behalf of a key. The relayer's key may be under a different identity than the subsidized key.
```mermaid
flowchart TD
MainDID["On-chain Identity (DID)"]
KeyToSubsidize["Key to be Subsidized"]
MainDID --> KeyToSubsidize
RelayerID["Relayer Identity"]
RelayerKey["Relayer Signing Key"]
RelayerID --> RelayerKey
KeyToSubsidize -.->|"Transaction fees subsidized by"| RelayerKey
MainDID -.->|"Can be the same identity"| RelayerID
```
---
## Fungible Assets
## Overview
Fungible assets on Polymesh represent digitized value such as shares, bonds, funds, stablecoins, and more. They are managed using the standardized Polymesh asset framework, ensuring all assets benefit from built-in compliance, settlement, and lifecycle management features. For a high-level introduction to assets and the Polymesh asset standard, see [Assets on Polymesh](/core/assets).
Fungible assets are interchangeable tokens with divisible or indivisible supply. Ownership is represented by balances held by different on-chain identities. They allow access to all core features such as compliance, settlement, corporate actions, metadata, and agent permissions, all available natively on-chain. For more on these features, see the relevant dedicated pages:
- [Compliance](/compliance)
- [Settlement](/settlement)
- [Asset Metadata](/core/assets/metadata)
- [Asset Agents & Permissions](/asset-agents)
- [Ownership Transfer](/core/assets/ownership-transfer)
- [Corporate Actions](/corporate-actions)
- [Security Token Offerings](/sto)
## Asset Creation
To create a fungible asset, users specify the asset type (e.g., Equity, Bond, Fund), divisibility, and optional metadata. Each asset is assigned a unique Asset ID and can have a unique ticker and external identifiers (such as ISINs, CUSIPs, etc.). For a step-by-step guide, see [Asset Creation](/core/assets#asset-creation).
## Issuance and Distribution
After creation, issuers or their appointed [agents](/asset-agents) can issue tokens to portfolios or accounts associated with their identity. Tokens can then be distributed to investors through a security token offering or directly using the settlement and compliance engines. See [Settlement](/settlement) and [Security Token Offerings](/sto) for more on fundraising and distribution.
## Key Features Unique to Fungible Assets
### Issuance and Redemption
- **Issuance**: An agent of the fungible asset can mint (issue) tokens to a portfolio or account under their on-chain identity (control of a portfolio can be assigned to another identity before issuing assets). This increases the total supply and the agents balance. Tokens are issued by calling the `asset::issue` method, specifying the `asset_id`, `amount`, and an `AssetHolderKind` (`Account`, `DefaultPortfolio`, or `UserPortfolio`) selecting the target.
- **Redemption**: Tokens can be redeemed (burned) from a portfolio or account owned by an appropriately permissioned agent of the asset, reducing both the total supply and the portfolio or account's balance. Tokens are redeemed by calling the `asset::redeem` method, specifying the `asset_id`, `value`, and an `AssetHolderKind` selecting the source to redeem the tokens from.
### Allowances
`asset::approve(asset_id, spender, amount)` lets an asset holder authorize another account (`spender`, typically a smart contract or other third party) to move up to `amount` of the asset on their behalf, without granting the spender any broader identity or portfolio permission. Calling `approve` again replaces any existing allowance for that `(owner, spender, asset_id)` combination rather than adding to it. Setting `amount` to `0` removes the allowance. The current allowance can be queried via the `AssetApi::allowance(owner, spender, asset_id)` runtime API.
:::note Unlimited allowances
Setting `amount` to the maximum value of the on-chain `Balance` type (`Balance::MAX`, i.e. the maximum `u128` value) grants an unlimited allowance: it is never decremented as the spender draws it down via `transfer_funds`. This is the standard way to grant an allowance without needing to track or periodically top up a specific remaining amount.
:::
The `spender` draws down the allowance by calling `settlement::transfer_funds`, naming the owner's account as the source — see [Direct Transfers](/settlement#direct-transfers-transfer_funds). This pairing (`approve` + `transfer_funds`) mirrors the ERC-20 `approve`/`transferFrom` pattern, and is the recommended way for a smart contract to move a user's tokens without requiring that user to co-sign every individual transfer. `approve` emits an `Approval` event when the allowance is set, and each draw-down via `transfer_funds` emits `AllowanceSpent` with the amount spent and the remaining allowance (unless the allowance is unlimited, in which case it isn't decremented). Allowances only apply to fungible assets — NFTs have no allowance concept.
### Divisibility
- Fungible assets can be created as divisible or indivisible. Divisibility is set at creation, but an indivisible asset can later be made divisible using the `asset::make_divisible` method (callable by the asset owner or agent). Once an asset is made divisible, this change is permanent and cannot be reversed.
- Divisibility affects the granularity of balances and transfers. For example, a divisible asset can represent shares with decimals, while an indivisible asset might represent whole-number units.
:::info
**Decimals:** All fungible assets on Polymesh use 6 decimals of precision. On-chain, balances are stored as integers, where 1 unit is represented as 1,000,000. If using the Polymesh SDK, it automatically converts between user-facing decimal values and the on-chain integer representation. Applications and user interfaces not using the SDK should account for this when displaying or entering amounts.
:::
### Corporate Actions & Checkpoints
- **Corporate Actions**: Fungible assets support on-chain corporate actions such as distributions and voting. These actions are tightly integrated with the asset's compliance and settlement features, allowing issuers to manage complex processes like dividend payments, shareholder meetings, and other corporate events directly on-chain. See [Corporate Actions](/corporate-actions) for more information.
- **Checkpoints**: Corporate actions rely on checkpoints, which are a feature of fungible assets only. Checkpoints are snapshots of all holder balances at a specific point in time. Checkpoints are essential for many corporate actions and compliance activities, such as dividend distributions, voting, and regulatory reporting. See [Checkpoint Management](/corporate-actions/checkpoints) for details.
### Transfer Restrictions
- **Transfer Restrictions**: In addition to identity-based on-chain compliance rules, Polymesh provides advanced transfer restriction features for fungible assets via the statistics pallet. These allow issuers to enforce count and percentage ownership restrictions such as a maximum investor count, maximum ownership percentage, and claim-based restrictions (e.g., jurisdiction, accreditation claim count restrictions). See [Transfer Restrictions](/compliance/transfer-restrictions) for details.
## Further Reading
- [Asset Management](/core/assets)
- [Non-Fungible Assets](/core/assets/nft)
- [Asset Metadata](/core/assets/metadata)
- [Asset Agents & Permissions](/asset-agents)
- [Ownership Transfer](/core/assets/ownership-transfer)
- [Checkpoint Management](/corporate-actions/checkpoints)
- [Corporate Actions](/corporate-actions)
- [Security Token Offerings](/sto)
- [Transfer Restrictions](/compliance/transfer-restrictions)
---
## Non-Fungible Assets
## Overview
The Non-Fungible Token (NFT) pallet extends the Asset pallet, allowing users to create NFT collections, issue unique tokens, and redeem (i.e. burn) existing non-fungible tokens. Documentation for the NFT crate can be found [here](https://docs.polymesh.live/pallet_nft/index.html).
NFTs on Polymesh are always managed as collections, each associated with a unique asset. **Before you can register local metadata keys or create an NFT collection with local keys, you must first create the asset itself as described in the [Asset Creation](/core/assets#asset-creation) section.** The asset must be of type `NonFungible`. Once the asset exists, you can register any required local metadata keys, and only then proceed to create the NFT collection. See [Asset Creation](/core/assets#asset-creation) for a step-by-step overview.
## NFT Collection
All non-fungible tokens are linked to a unique NFT collection, which is tied to an [Asset](/core/assets#asset-creation) and user-defined metadata.
### Collection Metadata Keys
Before creating an NFT collection, you must define [metadata](/core/assets/metadata) keys that specify the properties each NFT in the collection must have. **Registering local metadata keys requires that the asset has already been created.** These collection keys serve several purposes:
1. **Required Properties**: Each NFT in the collection must provide values for all defined collection keys.
2. **On-chain Data**: Values are stored directly on-chain for transparency and smart contract usage.
3. **Immutable Structure**: Collection keys and their values cannot be modified after creation of the collection or individual NFT.
To set up collection keys:
1. Register metadata keys after creating the asset but before creating the NFT collection, or identify appropriate standardized global metadata keys.
2. All required collection keys must be registered before collection creation.
3. The number of keys must not exceed MaxNumberOfCollectionKeys.
4. Keys can be registered individually or in batches.
### Creating an NFT Collection
To successfully create an NFT collection, the `create_nft_collection` function must be called, and the following conditions must be met:
- If the asset already exists, it must be of type `NonFungible`, and the caller must have the appropriate permission for the asset. If the asset does not exist, `create_nft_collection` will also create one non-fungible asset using the values passed as `nft_type`.
- This must be the first collection associated with the given `AssetId` (i.e. only one collection per Asset ID is allowed).
- The number of metadata keys associated with the collection must be less than or equal to `MaxNumberOfCollectionKeys`.
- All metadata keys must be registered before the collection is created. When using local metadata keys, you must call `create_asset` and `register_asset_metadata_local_type` before `create_nft_collection`.
Once the transaction succeeds, a unique ID is tied to the NFT collection, in addition to the Asset ID, which identifies the underlying asset details.
### Issuing an NFT
After creating the collection, tokens can be issued with the `issue_nft` function. For an NFT to be issued successfully, the following conditions must hold:
- An NFT collection associated with an `AssetId` must exist.
- The caller must have the appropriate permission for the `Asset`.
- The `AssetHolderKind` (`Account`, `DefaultPortfolio`, or `UserPortfolio`) passed to `issue_nft` selects the destination, and must be valid for the caller's DID.
- All metadata keys specified in the NFT collection must have a value set when issuing the token. Issuing a token with metadata keys not defined in the collection will fail.
Once the transaction succeeds, a unique non-fungible token is linked to the specified account or portfolio.
### Redeeming an NFT
A non-fungible token can be redeemed by calling the `redeem_nft` function. To successfully redeem an NFT, the following conditions must hold:
- An NFT collection associated with `AssetId` must exist.
- The caller must have the appropriate permission for the `Asset`.
- The `nft_id` of the token must exist in the caller's portfolio.
Once the transaction succeeds, the non-fungible token will no longer exist on chain and be removed from the caller's portfolio.
## NFTs and the Settlement Pallet
Polymesh's [Settlement](/settlement#overview) engine fully supports NFTs, and transfers of non-fungible tokens follow the same process as fungible assets. This means that all compliance rules defined for the underlying asset must be respected for a successful transfer of an NFT.
For a direct account-to-account transfer of specific NFTs without going through the settlement instruction/affirmation cycle, use `nft::transfer_nft(nfts, to, memo)`, or the more general [`transfer_funds`](/settlement#direct-transfers-transfer_funds) which also accepts portfolios on either side.
:::note
Allowances (`asset::approve`) do not apply to NFTs — only fungible assets support third-party, allowance-drawn transfers.
:::
## Metadata Specifications
NFTs support both collection-level and individual NFT metadata. Polymesh provides standardized approaches for handling metadata:
### On-chain vs Off-chain Storage
Unlike other chains and standards (e.g., EIP-721), Polymesh encourages metadata to be stored directly on-chain for:
- Greater transparency
- Reduced reliance on external systems
- Direct reference by smart contract business logic
However, off-chain storage options are also supported through standard URI specifications.
### Standardized Global Metadata Keys
Polymesh defines the following global metadata keys to enable standardized processing by third-party dApps. Additional standardized keys can be added through the onchain governance process:
1. **Token URI**: Points to off-chain JSON metadata
2. **Base Token URI**: Collection-wide base URI for token metadata
3. **Image URI**: Points to off-chain NFT image
4. **Base Image URI**: Collection-wide base URI for NFT images
### Token URI Specification
The Token URI of an NFT points to an off-chain JSON blob containing metadata. It can be specified through:
- Individual NFT's `tokenUri` global metadata key
- Collection's `baseTokenUri` global metadata key with optional `{tokenId}` substitution
Resolution process:
1. Use individual `tokenUri` if set
2. Otherwise, use `baseTokenUri` with `{tokenId}` substitution
3. If `baseTokenUri` lacks `{tokenId}`, append `/{tokenId}`
The referenced JSON should follow this schema:
```json
{
"title": "Token Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Identifies the asset this token represents"
},
"description": {
"type": "string",
"description": "Describes the asset this token represents"
},
"properties": {
"type": "object",
"description": "Arbitrary properties. Values may be strings, numbers, objects, or arrays."
}
}
}
```
### URI Resolution Process
Both Token and Image URIs follow a consistent resolution process:
1. Individual URI
- Check token-specific URI first (`tokenUri`/`imageUri`)
- Use if explicitly set for the individual NFT
Example Individual URIs:
```js
tokenUri: 'https://metadata.polymesh.network/tokens/abcdefg123';
imageUri: 'https://images.polymesh.network/tokens/abcdefg123.png';
```
2. Collection Base URI
- Fall back to collection's base URI (`baseTokenUri`/`baseImageUri`)
- Substitute `{tokenId}` placeholder if present
- Append `/{tokenId}` if no placeholder exists
Example Base URIs:
```js
baseTokenUri: "https://metadata.polymesh.network/collection/{tokenId}"
baseImageUri: "https://images.polymesh.network/collection/v1/{tokenId}.png"
baseImageUri: "https://images.polymesh.network/collection/v1" # expects the token id to be appended
```
:::note Notes
- Collection metadata keys are immutable after collection creation
- Values must be set for all the collection's metadata keys
- Image and token URI resolution follows the same priority order
- When using an image URI it is recommended to include the applicable file extension
:::
## Further Reading
- [Asset Management](/core/assets)
- [Fungible Assets](/core/assets/fungible)
- [Asset Metadata](/core/assets/metadata)
- [Asset Agents & Permissions](/asset-agents)
- [Ownership Transfer](/core/assets/ownership-transfer)
- [Settlement](/settlement)
---
## Confidential Assets
export default function ConfidentialRedirect() {
const to = useBaseUrl('/confidential-assets');
return ;
}
{/* This is a reference page that redirects to the main confidential assets documentation */}
---
## Asset Metadata
## Overview
Asset metadata provides a flexible and powerful way to associate additional information with assets on Polymesh. It enables storing both informational data and parameters that can drive automated business logic through smart contracts.
## Common Use Cases
- **Regulatory Information**: Store compliance and regulatory details
- **Asset Classifications**: Define asset types and categories
- **Investment Terms**: Document key investment parameters
- **External References**: Link to off-chain documentation
- **Business Logic**: Enable smart contract automation
- **NFT Properties**: Define required properties for NFT collections
- **Extended Asset Information**: Descriptions, issuer information etc.
## Metadata Types
Polymesh supports two distinct categories of metadata:
### Local Metadata
- Asset-specific metadata defined by issuers
- Managed by the asset issuer or their authorized agents
- Customizable per asset requirements
### Global Metadata
- Standardized metadata definitions usable across all assets
- Defined through on-chain governance
- Provide a consistent structure with asset-specific values
For example, a global metadata key might define a standardized way to specify "Risk Rating", but each asset would still set its own individual risk rating value using this standardized format.
## Structure
Each metadata entry has the following properties:
- **Key**: A unique identifier within its scope (local or global)
- **Name**: A human-readable identifier for the metadata field
- **Value**: The actual data stored in the metadata field
- **[Specifications](#metadata-specifications)** (Optional): Information such as an external url, description and type definition
- **Details** (Optional): Additional properties including expiry and lock status
## Metadata Management
### Registration and Usage
There are several ways to register and use metadata keys:
1. **Standard Two-Step Process**
- First register the metadata key using `register_asset_metadata_local_type` (or `register_asset_metadata_global_type` through the onchain governance process)
- Then set the value using `set_asset_metadata`
2. **Combined Registration and Setting**
- Use `register_and_set_local_asset_metadata` to register a local metadata key and set its value in a single transaction
- This is more efficient for initial setup of asset/collection level metadata
3. **Batched Operations**
- Use the utility batch function to combine multiple metadata operations in one transaction
- Particularly useful when setting up multiple metadata keys for an NFT collection
- Example: Register multiple collection keys and set their values in a single atomic transaction
### Value Management
Metadata values (not including NFT properties) can be:
- Set when registering the metadata
- Modified later (unless locked)
- Cleared when no longer needed
- Given an optional expiry date (informational only)
- Protected through various locking options
### NFT Collection Keys
When creating an NFT collection, metadata keys serve as collection keys that define required properties for each NFT in the collection. Refer to the [Non-Fungible Assets](/core/assets/nft/#collection-metadata-keys) page for more details.
## Metadata Specifications
The specification of each metadata entry is defined by the following optional properties that provide structure and context for the metadata values:
### Specification Components
1. **URL Reference**
- External reference URL relating to the metadata
- Can link to additional documentation or resources
- Useful for providing more context about the metadata
2. **Description**
- Human-readable description of the metadata's purpose
- Explains what the metadata represents
- Helps other parties understand the intended use
3. **Type Definition**
- Defines the format and structure of valid values
- Can specify SCALE encoding formats for complex data
- Can be used for offchain validation of metadata values
- Can be used to define a schema for smart contract use
### Value Types
Metadata values can be:
1. **Simple Values**
- Basic text strings for straightforward data
- Human-readable information
- Simple key-value pairs
2. **Complex Values**
- SCALE encoded data following the type definition
- Structured data that can be validated
- Machine-readable formats for automated processing
- Custom schemas for specific use cases
## Security and Access Control
### Locking Mechanisms
Metadata entries support three lock states:
1. **Unlocked**: Value can be freely modified
2. **Locked**: Value cannot be modified
3. **LockedUntil**: Value locked until a specified date
### Access Control
- Only asset issuers or [authorized agents](/asset-agents) can modify local metadata
- Global metadata modifications require governance approval
- Lock status can prevent modifications even by authorized parties
---
## Ownership Transfers
Transferring ownership of an asset or a ticker on Polymesh is a critical operation that allows the current issuer/owner to hand over full control to another identity. This is distinct from transferring asset balances and involves the rights to manage, configure, and control the asset or ticker. Tickers are unique but can be unlinked from an asset, allowing the ticker to be transferred to a new asset.
## Asset Ownership Transfer
Asset ownership transfer is a two-step process that uses the Polymesh [Authorizations](/authorizations) framework:
1. **Initiation (Authorization):** The current owner (issuer), or a sufficiently permissioned asset agent, initiates a transfer by calling `identity::add_authorization` to create an authorization request of type `TransferAssetOwnership` for the target identity.
2. **Acceptance:** The target identity must accept the authorization by calling `asset::accept_asset_ownership_transfer` and providing the associated authorization request ID for the transfer to complete. Until accepted, the transfer can be canceled by the original owner or rejected by the target identity using `identity::remove_authorization`.
### Who Can Initiate a Transfer
Asset ownership transfer is not restricted to the current owner. For the transfer to complete, the authorization must have been created by an identity that is a permissioned agent for the asset and whose agent group grants permission for **`asset::accept_asset_ownership_transfer`**:
| Agent group | Can initiate an asset ownership transfer? |
| :-------------- | :--------------------------------------------------------------------------------------------------- |
| `Full` | Yes. The current owner always qualifies, as the owner is automatically assigned to the `Full` group. |
| `ExceptMeta` | Yes. This group only excludes the `ExternalAgents` pallet, so the `Asset` pallet remains permitted. |
| `PolymeshV1PIA` | No. `Asset` pallet access is limited to `issue`, `redeem` and `controller_transfer`. |
| `PolymeshV1CAA` | No. Only the corporate action pallets are permitted. |
| Custom group | Yes, if the group's permissions cover `asset::accept_asset_ownership_transfer`. |
Creating the authorization is itself unrestricted: `identity::add_authorization` performs no asset-related validation, so any identity can create a `TransferAssetOwnership` request for any asset. The permission check runs when the recipient accepts, and a request created by an identity without the necessary permission simply fails at that point. A pending `TransferAssetOwnership` request in an identity's inbox is therefore not proof that the sender was entitled to send it.
Because the check uses chain state at acceptance time rather than at creation time, removing the authorizing agent, or moving them to a group that does not grant `asset::accept_asset_ownership_transfer`, before the recipient accepts will invalidate the pending request.
See [Asset Agents & Permissions](/asset-agents) for how agent groups and their permissions are defined.
This applies to asset ownership only. Ticker ownership transfer is checked differently: `asset::accept_ticker_transfer` requires the authorization to have been created by the ticker owner, so agents cannot initiate one.
:::warning
**Delegating agent permissions broadly can hand over the ability to give the asset away.** Any agent holding permission for **`asset::accept_asset_ownership_transfer`** can initiate a transfer of the entire asset to another identity. Issuers should review which agent groups grant this extrinsic, and which identities belong to them, as part of ongoing asset administration.
:::
:::info
If there is a **Ticker** linked to an asset, the ownership of that ticker will also transfer to the new asset owner.
:::
:::warning Important Warning
**Transferring asset ownership updates agent assignments automatically.** The previous owner is removed as an agent and the new owner is added as a `Full` agent when the transfer completes. Other agents and their permissions are unchanged.
After a transfer, the new owner should review and update the list of asset agents and their permissions to ensure only trusted parties retain access.
See [Asset Agents & Permissions](/asset-agents) for more information.
:::
## Ticker Ownership Transfer (Linking & Unlinking)
Tickers are unique symbols that can be linked to assets for a more human-readable way to identify an asset and for discoverability. Ticker ownership can also be transferred between identities using a similar authorization and acceptance process. A ticker with an active registration, or one that is fully owned and not linked to an asset, can be transferred.
- **Linking a Ticker:** Associates a registered ticker with an asset. Only the ticker owner can perform this action.
- **Unlinking a Ticker:** Removes the association between a ticker and an asset. Only the asset owner or authorized agent can perform this action.
- **Transferring Ticker Ownership:** The target identity must accept the authorization by calling `asset::accept_ticker_transfer` and providing the associated authorization request ID for the transfer to complete. Until accepted, the transfer can be canceled by the original owner or rejected by the target identity using `identity::remove_authorization`.
### Ticker Transfer Process
1. **Unlinking a Ticker:** If the ticker is currently linked to an asset and is not being transferred with the asset, it should first be unlinked by calling `asset::unlink_ticker_from_asset_id`.
2. **Initiation (Authorization):** The current owner initiates a ticker transfer by calling `identity::add_authorization` to create an authorization request of type `TransferTicker` for the target identity.
3. **Accept Transfer:** The new owner accepts the authorization to complete the transfer.
4. **Linking a Ticker:** Once the ownership has transferred, the new owner can link the ticker to a new asset by calling `asset::link_ticker_to_asset_id`.
## Summary
Transferring asset or ticker ownership is a powerful feature for managing the lifecycle of assets on Polymesh. Always ensure that after a transfer, agent permissions and related controls are reviewed and updated as needed to maintain security and compliance.
---
## Asset Agents & Permissions
## Overview
Issuers of regulated securities often delegate specific responsibilities, like managing documentation or compliance, to specialized service providers or internal teams. Polymesh facilitates this through **agents** and **agent groups**, providing granular control over permissions related to a specific asset.
The `externalAgents` pallet, in conjunction with the `identity` pallet, provides the necessary functionality to manage the entire lifecycle of agent permissions for an asset.
## Key Concepts
### Agents
An agent is any Polymesh [Identity](/identity/) that has been granted specific permissions to act on behalf of an asset. This includes the asset issuer, who is automatically designated as an agent with `Full` permissions upon asset creation, as well as any additional identities (such as service providers or internal staff) that are granted agent status.
### Agent Groups
Permissions are not assigned directly to agents but rather to **Agent Groups**. Each agent assigned to an asset belongs to exactly one group for that asset. An asset can have multiple agent groups, each defining a different set of permissions.
### Permissions
Polymesh employs a hierarchical permission system allowing fine-grained control. Permissions determine which actions (extrinsics within specific pallets) an agent belonging to a group can perform for the asset.
Permissions can be defined at the group level using three main types:
- **Full**: Grants access to all extrinsics across all pallets related to the asset.
- **These**: Grants access _only_ to specified extrinsics within specified pallets.
- **Except**: Grants access to all extrinsics _except_ for specified ones within specified pallets.
This allows for precise delegation, such as granting an agent permission only to manage asset documentation (`asset::add_documents`, `asset::remove_documents`) but nothing else.
## Permission Groups
### Predefined Groups
Polymesh offers several built-in agent groups for common use cases:
| Group Name | Description |
| :-------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Full` | Grants all permissions for the asset. |
| `ExceptMeta` | Grants all permissions _except_ those related to managing agent identities and permissions (`externalAgents` pallet). |
| `PolymeshV1CAA` | Grants permission to all extrinsics in the CorporateAction, CorporateBallot and CapitalDistribution pallets |
| `PolymeshV1PIA` | Grants permission to the `issue`, `redeem` and `controller_transfer` extrinsics from the `Asset` pallet and all extrinsics in the STO pallet, except `sto::invest` |
:::warning
`ExceptMeta` withholds agent management, but it does **not** withhold ownership transfer. Because it excludes only the `ExternalAgents` pallet, an `ExceptMeta` agent is still permitted to call `asset::accept_asset_ownership_transfer`. Such an agent cannot add or remove agents directly, but can transfer ownership of the asset to another identity, which indirectly hands every agent right to the new owner.
:::
### Custom Groups
Custom agent groups allow issuers to define highly specific permissions for agents on a per-asset basis.
#### Creating a Custom Agent Group
Custom agent groups are initially created using the `externalAgents::create_group` method.
**Parameters:**
- `asset_id`: The AssetId (UUID) of the asset.
- `perms`: The permissions for the group, defined as an `ExtrinsicPermissions` structure (see below).
**Effects:**
- Creates a new custom agent group for the asset, assigning it a unique group ID (`AGId`). The group ID is used when inviting or assigning agents.
**Errors:**
- Creating a permission group can fail if the caller is not authorized or if the permissions structure is too large.
#### Defining Permissions: `ExtrinsicPermissions` Structure
The `perms` parameter uses the `ExtrinsicPermissions` structure, which allows for fine-grained control over what actions an agent group can perform. Permissions are defined in two layers:
1. **Pallet Level**: Specify which pallets the group can interact with.
2. **Extrinsic Level**: For each pallet, specify which extrinsics (methods) are allowed or denied.
At both levels, you can use:
- `Whole`: All pallets or all extrinsics in a pallet.
- `These`: Only the specified pallets or extrinsics.
- `Except`: All except the specified pallets or extrinsics.
**Example:**
- To allow only documentation management, set `perms` to allow only the `Asset` pallet and the pallet's `add_documents` and `remove_documents` extrinsics:
- At the pallet level: `These([Asset])`
- At the extrinsic level for `Asset`: `These([add_documents, remove_documents])`
:::info
When defining custom agent permissions, pallet names use `UpperCamelCase` (e.g., `Asset`), and extrinsic names use `snake_case` (e.g., `add_documents`). The chain metadata can be used to identify supported pallets and functions. [Subscan](https://polymesh.subscan.io/runtime) offers a convenient tool for exploring the chain metadata to identify correct pallet and extrinsic names.
:::
:::warning
When using `Except` at either the pallet or extrinsic level, be aware that future chain upgrades may add new pallets or extrinsics. Agents in a group with `Except` permissions could automatically gain access to new functionality, which may not be intended. For maximum security, prefer using `These` to explicitly enumerate allowed actions.
:::
:::warning
Carefully consider whether to grant access to the `ExternalAgents` pallet when defining custom group permissions. Granting permission to this pallet allows agents to modify agent groups and permissions including their own. To prevent agents from escalating their privileges or altering group membership, it is recommended to **not include** the `ExternalAgents` pallet unless such administrative control is intended.
:::
:::warning
Carefully consider whether to grant the `Asset` pallet's `accept_asset_ownership_transfer` extrinsic. An agent permitted to call it can create a `TransferAssetOwnership` authorization that hands the entire asset to another identity.
This permission is easy to grant unintentionally. A group granted the whole `Asset` pallet — a natural shape for an "asset manager" group — includes it, as does any `Except` form at either the pallet or extrinsic level that does not explicitly exclude it. This is distinct from the forward-compatibility concern above: the extrinsic exists today, and is included the moment the group's permissions are broad enough to cover it.
To withhold it, enumerate the permitted extrinsics with `These`, or add `accept_asset_ownership_transfer` to an `Except` list. See [Ownership Transfers](/core/assets/ownership-transfer) for how the permission is evaluated.
:::
#### Modifying Custom Groups
- Use `externalAgents::set_group_permissions(asset_id, id, perms)` to update the permissions for an existing custom group. This method is identical to the `create_group` method described above but takes an additional `id` parameter for the agent group too be modified.
## Agent Lifecycle
Managing agents involves a multi-step process at the chain level:
### 1. Invitation
An existing agent with permission to manage agents invites a new agent by creating an authorization request using the `identity::addAuthorization` extrinsic (from the `identity` pallet), with `AuthorizationData::BecomeAgent` specifying the asset and agent group.
### 2. Acceptance
The invited agent must accept the authorization using the `externalAgents::accept_become_agent` extrinsic, providing the authorization ID. This links the agent to the specified group for the asset.
- If the group's permissions are later changed, the agent's permissions update automatically.
- The agent can reject the authorization using `identity::removeAuthorization`.
### 3. Changing Group
An agent's group can be changed by an authorized agent using the `externalAgents::change_group` extrinsic.
### 4. Abdication (Self-Removal)
An agent can remove themselves from an asset using the `externalAgents::abdicate` extrinsic. The last `Full` agent cannot abdicate until another `Full` agent is added.
### 5. Removal
An authorized agent can remove another agent using the `externalAgents::remove_agent` extrinsic. The last `Full` agent cannot be removed.
## Default Permissions
When an Identity creates an asset, that Identity is automatically assigned to the `Full` permission group for that asset as an agent.
## Ownership Transfer Considerations
:::warning Critical Note
Agent permissions are tied to the asset, not the asset owner's Identity. When asset ownership (control) is transferred to a new Identity, the previous owner is removed as an agent and the new owner is added as a `Full` agent automatically. Other agents and their permissions remain unchanged, so managing agent permissions is still a key step during ownership changes.
:::
Agents are not only affected by an ownership transfer. A sufficiently permissioned agent can also initiate one. Any agent whose group grants `asset::accept_asset_ownership_transfer`, including all `Full` and `ExceptMeta` agents, can create the authorization that transfers the asset. See [Ownership Transfers](/core/assets/ownership-transfer) for the full details of which identities qualify and when the check is applied.
## SDK Integration
The Polymesh SDK provides convenient methods to interact with the chain extrinsics and storage described above. For example, the SDK's `Permissions` class wraps calls to `externalAgents::create_group`, `externalAgents::remove_agent`, and related extrinsics, and provides helpers for querying agent groups and permissions. See the [SDK documentation](/sdk-docs/) for details.
### Permissions (Asset Side)
The [`Permissions`](/sdk-docs/api/entities/Asset/Base/Permissions/classes/Permissions/) class (accessed via `asset.permissions`) provides methods for managing and querying an asset agent roles and permissions across a asset:
- [`inviteAgent`](/sdk-docs/api/entities/Asset/Base/Permissions/classes/Permissions/#inviteagent): Invites an Identity to become an agent for the asset within a specific group. Handles the `identity::addAuthorization` call.
- [`removeAgent`](/sdk-docs/api/entities/Asset/Base/Permissions/classes/Permissions/#removeagent): Removes an agent associated with the asset. Handles the `externalAgents::removeAgent` call.
- [`createGroup`](/sdk-docs/api/entities/Asset/Base/Permissions/classes/Permissions/#creategroup): Creates a new custom permission group. Handles the `externalAgents::createGroup` call.
- [`getAgents`](/sdk-docs/api/entities/Asset/Base/Permissions/classes/Permissions/#getagents): Retrieves a list of agents and their associated groups for the asset.
- [`getGroup`](/sdk-docs/api/entities/Asset/Base/Permissions/classes/Permissions/#getgroup): Retrieves details for a specific permission group.
- [`getGroups`](/sdk-docs/api/entities/Asset/Base/Permissions/classes/Permissions/#getgroups): Retrieves all permission groups associated with the asset.
### AssetPermissions (Identity Side)
The [`AssetPermissions`](/sdk-docs/api/entities/Identity/AssetPermissions/classes/AssetPermissions/) class provides methods for managing and querying an Identity's agent roles and permissions across assets:
- [`get`](/sdk-docs/api/entities/Identity/AssetPermissions/classes/AssetPermissions/#get): Retrieve all assets over which this Identity has agent permissions, with the corresponding permission group.
- [`getGroup`](/sdk-docs/api/entities/Identity/AssetPermissions/classes/AssetPermissions/#getgroup): Retrieve this Identity's permission group for a specific asset.
- [`checkPermissions`](/sdk-docs/api/entities/Identity/AssetPermissions/classes/AssetPermissions/#checkpermissions): Check whether this Identity has specific transaction permissions over an asset.
- [`setGroup`](/sdk-docs/api/entities/Identity/AssetPermissions/classes/AssetPermissions/#setgroup): Assign this Identity to a different permission group for a given asset.
- [`waive`](/sdk-docs/api/entities/Identity/AssetPermissions/classes/AssetPermissions/#waive): Abdicate from the current permission group for a given asset (removes all permissions for that asset).
Acceptance of an authorization request to become an agent is done with the [Authorizations](/sdk-docs/api/entities/Identity/classes/Identity/#authorizations) (accessed via `identity.authorizations`).
## Summary
Polymesh's agent system provides a robust and granular framework for delegating asset management responsibilities. By leveraging predefined and custom agent groups, issuers can securely grant specific permissions to third parties or internal teams, ensuring compliance and operational efficiency while adhering to the principle of least privilege. The two-stage invitation/acceptance process adds a layer of security, requiring explicit consent from the designated agent.
---
## Security Token Offerings
## Overview
A Security Token Offering (STO) is a fundraising method that involves making tokens representing ownership of a real-world asset or security available for purchase. Polymesh provides native on-chain capabilities for conducting token offerings through its STO functionality.
STOs allow asset issuers to raise capital by selling tokens to investors in exchange for another asset, typically a stablecoin. The process is managed on-chain with automated settlement, ensuring transparency and efficiency throughout the offering lifecycle.
Common use cases include initial token offerings (primary issuance), secondary offerings (additional funding rounds), capital raises for specific projects, private placements to qualified investors, and rights issues to existing token holders.
:::note Regulatory Considerations
The specific steps and requirements for launching an STO on Polymesh may vary depending on your specific needs and the legal and regulatory requirements in your jurisdiction. Selling security tokens results in the application of an extensive set of laws, regulations, and potential liabilities. It's always advisable to consult with legal and financial experts to ensure that you comply with all applicable laws and regulations before you proceed with your STO.
Polymesh does not provide any advice or assurance your security tokens comply with applicable laws and regulations.
:::
## Core Concepts
- **Offering Asset**: The security token being offered for sale (identified by its `AssetId`). Note that Polymesh's native STO functionality only supports [Fungible assets](/core/assets/fungible).
- **Raising Asset**: The asset (typically a stablecoin) accepted as payment for the offering asset.
- **Price Tiers**: Configurable pricing levels that can offer different volumes of tokens at different price points.
- **Time Window**: The period during which the STO is active, defined by start and end times.
- **Venue**: The [settlement venue](/settlement/venues) where the exchange of assets takes place.
- **Minimum Investment**: The minimum amount of the raising asset required to participate.
## STO Lifecycle
The STO process involves several key stages, each managed through specific transactions in the Polymesh ecosystem.
### 1. Creating an STO
Asset issuers or their appointed [agents](/asset-agents) can create an STO by calling the `sto::create_fundraiser` transaction.
**Parameters**:
- `offering_portfolio`: Portfolio containing the `offering_asset`.
- `offering_asset`: Asset being offered for sale.
- `raising_portfolio`: Portfolio where the `raising_asset` will be deposited.
- `raising_asset`: Asset being accepted as payment.
- `tiers`: Price tiers to charge investors, each with a `total` amount available and a `price` per unit.
- `venue_id`: ID of the settlement venue (must be of type `STO`).
- `start`: Fundraiser start time (optional, defaults to immediate start).
- `end`: Fundraiser end time (optional, defaults to no expiration).
- `minimum_investment`: Minimum amount of `raising_asset` required to invest.
- `fundraiser_name`: Descriptive name for the fundraiser (informational only).
**Effects**:
- Creates an STO record on-chain with a unique `FundraiserId` specific to the offering asset.
- Locks the offering asset in the offering portfolio (the locked amount equals the sum of all totals from the defined price tiers).
- Sets up the price tier structure and time window for the STO.
- Creates a permanent record of the fundraiser in the chain state.
**Constraints**:
- Can only be executed by identities with agent permissions for the offering asset.
- The calling identity must have [custodial control](/portfolios/custody) of both the offering [portfolio](/portfolios) and raising portfolio, but does not need to directly own them.
- Requires a valid venue of type `STO`.
- Price tiers must be valid (i.e each with non-zero totals).
- If both start and end times are specified, start must be before end.
### 2. Investing in an STO
During the active period of an STO, eligible investors can participate by calling the `sto::invest` transaction.
**Parameters**:
- `investment_portfolio`: Portfolio where purchased tokens will be deposited.
- `funding_portfolio`: Portfolio that will fund the investment.
- `offering_asset`: Asset to invest in.
- `id`: ID of the fundraiser to invest in.
- `funding`: Can be set to `OnChain` or `OffChain`. On-chain funding requires the funding portfolio to be provided. Off-chain funding requires a payment [receipt](#off-chain-receipt-structure) from an authorized [venue receipt signer](/settlement/off-chain/#receipt-signers) to be provided.
- `purchase_amount`: Number of offering tokens to purchase (not the investment amount in raising asset).
- `max_price`: Maximum blended price per token willing to pay (optional, no constraint if omitted). This protects against unexpected pricing when investments span multiple tiers.
**Effects**:
- Calculates the total investment cost based on the tiered pricing structure and purchase amount.
- Validates that the blended price doesn't exceed the investor's maximum price (if specified).
- Creates and executes a settlement instruction exchanging the calculated cost in raising asset for the requested tokens.
- Updates the remaining available amounts in each affected price tier.
- Records the investment transaction on-chain.
**Constraints**:
- Can only be executed during the active STO period (after start and before end).
- The investor must meet all compliance requirements for both the offering and raising assets.
- The requested purchase amount must be available across the active tiers.
- The calculated investment cost must meet or exceed the minimum investment threshold.
- If a maximum price is specified, the blended price must not exceed this limit.
### 3. Managing an Active STO
Several management functions are available to the STO creator during its lifecycle:
#### Modifying the Time Window
The STO time window can be adjusted using `sto::modify_fundraiser_window`.
**Parameters**:
- `offering_asset`: Asset the offering being modified relates to.
- `id`: ID of the fundraiser to modify.
- `start`: New start time.
- `end`: New end time (optional).
**Effects**:
- Updates the start and/or end times of the STO.
- Records the modification on-chain.
**Constraints**:
- Can only be executed by identities with agent permissions for the offering asset.
- Cannot modify a closed STO.
- If both start and end are specified, start must be before end.
#### Freezing and Unfreezing
STOs can be temporarily paused using `sto::freeze_fundraiser` and resumed using `sto::unfreeze_fundraiser`.
**Parameters**:
- `offering_asset`: Target asset of the fundraiser.
- `id`: ID of the fundraiser to freeze/unfreeze.
**Effects**:
- Changes the STO status to `Frozen` (preventing investments) or `Live` (allowing investments).
- Records the status change on-chain.
**Constraints**:
- Can only be executed by identities with agent permissions for the offering asset.
- Cannot freeze or unfreeze a closed STO.
### 4. Stopping an STO
An STO must be explicitly stopped to finalize it and release any unsold tokens. This applies both to STOs that are being ended early and those that have reached their natural end date. Stopping an STO is performed by calling the `sto::stop` transaction.
**Parameters**:
- `offering_asset`: Asset to stop.
- `id`: ID of the fundraiser to stop.
**Effects**:
- Changes the STO status to `Closed` or `ClosedEarly` (depending on timing).
- Unlocks any remaining unsold offering tokens in the offering portfolio, making them available for other uses.
- Prevents any further investments in the STO.
- Records the closure on-chain.
**Constraints**:
- Can only be executed by identities with agent permissions for the offering asset.
- Cannot stop an already closed STO.
## Querying STO Information
Investors and other interested parties can access information about active STOs through several query methods:
- `sto::fundraisers`: Retrieve details about specific fundraisers.
- `sto::fundraiser_names`: View fundraiser names for a given asset.
- `sto::fundraiser_count`: Check the total number of fundraisers for an asset.
## Technical Considerations
### Tiered Pricing Mechanism
The STO functionality supports tiered pricing, allowing issuers to offer different volumes of tokens at different price points. Key aspects of this mechanism:
- Each tier has a total amount available and a fixed price per unit.
- Tiers are consumed in sequence, from the lowest index to the highest.
- A single investment can span multiple tiers if purchasing a large amount.
- The price is calculated as a weighted average (blended price) when spanning multiple tiers.
#### Investment Amount vs. Purchase Amount
It's important to understand the distinction between investment amount and purchase amount:
- **Purchase Amount**: The number of offering tokens the investor wants to acquire (specified in the `sto::invest` transaction)
- **Investment Amount**: The total cost in the raising asset required to purchase those tokens (calculated by the system based on tier pricing)
#### Blended Pricing and Max Price Protection
When an investment spans multiple tiers, the system:
1. **Calculates the total cost** by applying tier pricing sequentially
2. **Determines the blended price** as: `total_cost / purchase_amount`
3. **Applies max price protection** (if specified): The blended price cannot exceed the investor's `max_price` parameter
**Example Scenario:**
- Tier 1: 1,000 tokens at $1.00 each (500 tokens remaining)
- Tier 2: 1,000 tokens at $1.50 each (1,000 tokens remaining)
- Investor wants to purchase 800 tokens with max_price of $1.20
**Calculation:**
- 500 tokens from Tier 1: 500 × $1.00 = $500
- 300 tokens from Tier 2: 300 × $1.50 = $450
- Total cost: $950 for 800 tokens
- Blended price: $950 ÷ 800 = $1.1875 per token
- Since $1.1875 < $1.20 (max_price), the investment succeeds
If the blended price exceeded $1.20, the transaction would fail with `MaxPriceExceeded` error, protecting the investor from unexpected pricing.
### Compliance Integration
All investments in an STO are subject to the compliance rules of both the offering and raising assets:
- Investors must be eligible to receive the offering asset (passing its compliance and transfer restrictions).
- Investors must be eligible to transfer the raising asset (passing its compliance and transfer restrictions).
- The STO mechanism integrates with Polymesh's compliance framework to enforce these rules.
### Settlement Process
The investment process uses Polymesh's settlement system:
- Each investment creates a settlement instruction with two legs:
1. Transfer of offering tokens from the offering portfolio to the investor's portfolio.
2. Transfer of raising assets from the investor's funding portfolio to the raising portfolio.
- The instruction is automatically affirmed by both parties.
- Settlement occurs immediately upon successful affirmation.
### Portfolio Implications
Both the STO creator and investors must consider portfolio management:
- **For Creators**: The offering portfolio must contain sufficient tokens, which become locked during the STO exclusively for distribution by the offering.
- **For Investors**: The funding portfolio must contain sufficient raising assets, and the offering tokens will be deposited into the investment portfolio.
### Off-Chain Receipt Structure
For STOs that support off-chain funding, investors must provide a signed receipt. The receipt structure includes:
**Required Receipt Data:**
- The unique receipt ID (UID)
- The fundraiser ID
- The sender's identity (DID)
- The receiver's identity (DID)
- The raising asset ticker symbol/identifier
- The equivalent investment amount in the raising asset (calculated based on STO tier pricing)
:::important Amount Calculation
The amount in the off-chain receipt must represent the equivalent value in the raising asset, not the off-chain asset amount. For example, if an investor transfers 1 BTC off-chain but the STO uses a USD stablecoin as the raising asset, the receipt amount should reflect the USD equivalent value based on the STO's tier pricing structure.
:::
:::warning Exact Amount Requirement
The receipt amount must be **exactly** the investment cost calculated by the STO's [blended pricing mechanism](#blended-pricing-and-max-price-protection). Any deviation from this calculated amount will cause the settlement to fail during signature verification. The investment amount is determined by applying tier pricing sequentially to the requested purchase amount, and the receipt must reflect this precise calculation.
:::
**Signature Generation:**
- The data elements are SCALE encoded
- The encoded data is wrapped with `` and `` tags to form the final payload
- An authorized venue signer creates a cryptographic signature of this wrapped payload
- The signature must be generated using supported key types (SR25519, ED25519, ECDSA)
**Receipt Assembly:**
- The final receipt combines:
- The receipt UID
- The signer's account information
- The generated signature
- Optional metadata about the investment
:::note
The payload data must be wrapped with `` and `` tags before signing. The Polymesh SDK provides helper functions to generate valid STO receipts with the correct format.
:::
---
## Assets on Polymesh
Assets play a central role on Polymesh, enabling the representation of everything from traditional securities to digital collectibles. Polymesh provides a robust, purpose-built framework for the creation and management of both [Fungible Assets](/core/assets/fungible) and [Non-Fungible Assets](/core/assets/nft) (NFT) assets. Rich asset features and functionality are available natively on-chain without the need for custom smart contract development, although smart contracts are also available for advanced use cases. All native assets share a standard interface, allowing core chain functionality and integrations to work seamlessly with any Polymesh native asset. This means that if an integration supports the standard asset interface, it can support all native assets on Polymesh, without requiring new integrations for individual smart contracts.
## The Polymesh Asset Standard
A common question is: "What token standard do Polymesh assets comply with?" Unlike smart contract platforms, where each token may implement its own variation of a standard (such as ERC-20 or ERC-721), Polymesh defines its asset standard directly in the blockchain runtime. This means all assets, whether fungible or non-fungible, are governed by the same robust, chain-level logic and interface. The standard is enforced by the chain itself, not by individual smart contracts.
We refer to these as **Polymesh Native Assets** to distinguish them from assets implemented via smart contracts. However, note that the term "native asset" does not refer to the POLYX utility token, which is the native protocol token of Polymesh. Instead, Polymesh Native Assets are any assets (securities, tokens, NFTs, etc.) created and managed using the Polymesh asset framework.
This approach ensures:
- Consistency and predictability for all assets on the network
- Built-in compliance, settlement, and management features
- Seamless integration for wallets, custodians, and other infrastructure
- No need for custom code or audits for each new asset type
If an integration or application supports the Polymesh asset standard, it will work with all assets on the chain, without requiring new work for each asset or smart contract implementation.
## What is an Asset?
An **asset** on Polymesh is a uniquely identified, on-chain record that can represent any digitized value such as equity, bonds, funds, stablecoins, or NFTs. Each asset is assigned a unique 16-byte `AssetId` (UUID) at creation, which is used throughout the chain to reference and manage the asset. Assets can be:
- **Fungible**: Interchangeable tokens with divisible or indivisible supply (e.g., shares, bonds, stablecoins). See [Fungible Assets](/core/assets/fungible).
- **Non-Fungible**: Unique tokens, each with distinct properties, managed in collections (e.g., NFTs, certificates). See [Non-Fungible Assets](/core/assets/nft).
## Asset Lifecycle Overview
1. **Register Ticker (optional):** Reserve a unique ticker for your asset (fee applies).
2. **Create Asset:** Define asset properties and create on-chain, receiving a unique `AssetId`.
3. **Link Ticker:** Optionally link a registered ticker to the asset.
4. **Configure Compliance & Metadata:** Set compliance rules, metadata, and external identifiers.
5. **Appoint Agents:** Delegate management to external agents as needed.
6. **Issue Tokens:** Mint tokens to portfolios you control, then distribute via settlement instructions.
7. **Ongoing Management:** Update documentation, metadata, compliance, and manage corporate actions as required.
## Asset Creation
Assets are created on-chain by an identity (the issuer) using either the `create_asset` or `create_asset_with_custom_type` methods. You must specify the [asset name](#asset-name-renaming), [divisibility](/core/assets/fungible/#divisibility), [asset type](#asset-types), and can optionally include [external identifiers](#securities-identifiers) and a [funding round](#funding-round) name. Each asset receives a unique `AssetId`.
- **Fungible Assets:** Created directly with the chosen type and properties.
- **NFTs:** After creating the asset, you must register any local NFT keys or metadata types to the asset before creating the NFT collection. See [Non-Fungible Assets](/core/assets/nft) for details. If specifying local NFT keys, always create the asset first, then register keys, then create the collection.
Optionally, a unique ticker can be registered and linked to the asset, providing a human-readable symbol. Ticker registration is time-limited and transferable. See [Ticker Registration](#ticker-registration).
For additional asset type specific functionality see [Fungible Assets](/core/assets/fungible) and [Non-Fungible Assets](/core/assets/nft).
## Core Features of Assets
Polymesh assets share a set of powerful, standardized features, regardless of type. For details on each, see the linked dedicated pages:
- **Ownership & Control:** Asset ownership is always at the identity level. The issuer can transfer ownership via on-chain authorization. Asset control can be delegated to [Asset Agents](/asset-agents) with granular permissions. See [Ownership Transfer](/core/assets/ownership-transfer).
- **Compliance & Settlement:** All asset transfers are subject to compliance rules defined by the issuer. Transfers are executed through the [Settlement](/settlement) engine, ensuring all rules are enforced. See [Compliance](/compliance).
- **Metadata & Identifiers:** Assets can have both local and global metadata, and can be assigned standard securities identifiers (ISIN, CUSIP, etc.). See [Asset Metadata](/core/assets/metadata) and the [Securities Identifiers](#securities-identifiers) section below.
- **Documentation:** Issuers can link off-chain documents (prospectuses, filings, etc.) to assets, storing references and hashes on-chain for auditability. See [Document References](#document-references).
- **Issuance & Redemption:** Issuers or authorized agents can mint (issue) or burn (redeem) tokens for an asset, with all actions recorded on-chain. See [Fungible Assets](/core/assets/fungible) and [Non-Fungible Assets](/core/assets/nft) for details.
- **Checkpoints & Corporate Actions:** _(Fungible assets only)_ Assets support creation of [checkpoints](/corporate-actions/checkpoints) to snapshot balances at specific times, enabling distributions and voting. See [Corporate Actions](/corporate-actions). NFTs do not support checkpoints or corporate actions, see [Non-Fungible Assets](/core/assets/nft) for details.
- **Advanced Controls:** Asset issuers/agents can freeze/unfreeze transfers, set mandatory mediators, manage venue restrictions, and more. See [Freezing and Unfreezing Assets](#freezing-and-unfreezing-assets) and [Controller Transfers](#controller-transfers).
## Ticker Registration
Tickers are optional, unique symbols that can be linked to assets for easier reference. Tickers can be associated with both fungible assets and non-fungible asset collections. They offer an additional unique identifier that can be defined by the asset owner for a more convenient way to reference an asset.
1. **Register Ticker:** Reserve a unique ticker for 60 days by calling the `asset::register_unique_ticker` function. Ownership of the registration is assigned to the caller. If not linked to an asset within 60 days, the ticker registration will expire and become available to others.
2. **Link Ticker:** Link the registered ticker to the asset by calling `asset::link_ticker_to_asset_id`.
3. **Unlink Ticker:** The asset owner or an authorized agent can unlink the ticker from the asset using `asset::unlink_ticker_from_asset_id`. This does not affect the ticker registration itself, which remains owned by the original owner.
4. **Ticker Ownership Transfer:** Ticker ownership can be transferred to another identity. See [Ownership Transfer](/core/assets/ownership-transfer) for details.
## Asset Name (Renaming)
The asset name is a human-readable label for the asset, set at creation and updatable by the owner or agent using the `asset::rename_asset` method. Renaming does not affect the asset's operation or identifiers, but helps with discoverability and clarity for users and integrators. Asset names are not unique and can be shared across different assets. However, the asset name can be important for user interfaces, wallets, and other applications that display asset information.
## Funding Round
The funding round is an optional, descriptive label (e.g., "Seed Round", "Series A") that can be set at asset creation or updated later with `asset::set_funding_round`. Its primary purpose is to categorize and track token issuance events for financial reporting, compliance, and audit trails. The system records how many tokens are issued in each funding round, and this information is included in on-chain events and distribution records.
## Asset Types
Polymesh supports a wide range of asset types, each designed for specific regulatory, operational, or business needs. The asset type determines whether the asset is fungible or non-fungible and can affect which features are available (e.g., checkpoints, transfer restrictions, NFT collections). Asset types also help with regulatory classification and reporting.
- **Equity (common, preferred):** Represents shares in a company, either common or preferred stock. Used for traditional equity securities.
- **Bonds and fixed income:** Represents debt instruments, such as bonds or notes, with fixed or variable returns.
- **Funds and REITs:** Used for pooled investment vehicles, including mutual funds, ETFs, and real estate investment trusts.
- **Commodities:** Represents physical or digital commodities, such as gold, oil, or tokenized resources.
- **Stablecoins:** Pegged-value tokens, typically representing fiat currencies or other stable assets.
- **Derivatives:** Contracts whose value is derived from other assets, such as options or futures.
- **Revenue share agreements:** Represents claims on a share of revenue from a business or project.
- **Structured products:** Custom financial instruments combining multiple asset types or features.
- **Custom asset types (user-defined):** Issuers can register and use custom asset types for specialized or novel use cases, using `asset::register_custom_asset_type`.
- **Non-fungible tokens (NFTs):** Unique tokens managed in collections, used for digital collectibles, certificates, or other one-of-a-kind assets. NFT types enable features specific to collections and unique ownership.
The asset type is set at creation and can only be updated within the same fungibility class (fungible to fungible, NFT to NFT) using `asset::update_asset_type`. Asset type is not just metadata: it determines how the asset behaves on-chain. For example, only assets with a fungible type can use certain features (like checkpoints, transfer restrictions, or redemption), while NFT types enable collection and unique token management. Custom asset types can be registered with `asset::register_custom_asset_type` and used for specialized or user-defined asset categories.
## Securities Identifiers
Securities identifiers (such as ISIN, CUSIP, LEI, CINS, and FIGI) can be assigned to Polymesh assets to help them integrate with global financial systems and meet regulatory requirements. These identifiers are informational and make assets more easily recognizable and interoperable with custodians, exchanges, and other infrastructure.
Identifiers can be set during asset creation or updated later by the asset owner or an authorized agent using the `asset::update_identifiers` function. All identifiers are validated for correct format and changes are recorded on-chain for transparency.
## Document References
Polymesh allows asset owners and authorized agents to attach references to off-chain documents directly to an asset. Document references can include a URI (such as a URL, IPFS address, or other location), a document name, and an optional hash of the document content. The referenced documents themselves can be publicly accessible, encrypted, or protected behind secure login as appropriate for the use case.
- **Adding Documents:** Use `asset::add_documents` to attach one or more document references to an asset. Including a hash allows anyone to verify the integrity of the off-chain document by comparing it to the hash stored on-chain.
- **Removing Documents:** Use `asset::remove_documents` to remove document references from an asset. All changes are recorded immutably on-chain, creating a transparent audit trail.
This functionality provides a tamper-evident, on-chain record of all documents associated with an asset, supporting compliance, due diligence, and independent verification for regulatory or legal purposes.
## Controller Transfers
Controller transfers allow the asset owner or an authorized agent to forcibly transfer tokens or NFTs from one portfolio to another, typically for regulatory or legal enforcement scenarios. This feature is available for both fungible and non-fungible assets, but the method and parameters differ slightly:
- **Fungible Assets:** Use the `asset::controller_transfer` method. Parameters are `asset_id`, `value` (amount to transfer), `source` (the [`AssetHolder`](/asset-holdings) — account or portfolio — to debit), and `destination_kind` (an `AssetHolderKind` selecting where on the caller's identity the tokens land: `Account`, `DefaultPortfolio`, or `UserPortfolio`).
- **Non-Fungible Assets (NFTs):** Use the `nft::controller_transfer` method. Parameters are `nfts` (the collection and specific NFT IDs to transfer), `source` (the `AssetHolder` — account or portfolio — to debit), and `destination_kind` (an `AssetHolderKind` selecting where on the caller's identity the NFTs land).
:::info
**Note:** Assets that are locked in a settlement instruction cannot be transferred using controller transfer. While controller transfers bypass many compliance and restriction checks, they cannot bypass basic balance checks. If tokens or NFTs are locked (e.g., as part of a pending settlement), controller transfer will fail due to insufficient available balance.
:::
Controller transfers are powerful tools for asset issuers and agents, but should be used with care as they override normal transfer restrictions and compliance checks.
## Freezing and Unfreezing Assets
Freezing and unfreezing assets or NFT collections is an important control for issuers and agents, allowing them to quickly respond to regulatory, compliance, or emergency situations. For example, freezing may be required if there is a suspected security breach, regulatory investigation, or to prevent unauthorized transfers during a corporate action or legal dispute.
Assets and NFT collections on Polymesh can be frozen or unfrozen by the asset owner or an authorized agent. When an asset is frozen, regular transfers are blocked, including settlements, STO distributions, and portfolio movements.
- **Freeze:** Call `asset::freeze` with the `asset_id` to prevent standard transfers of the asset or NFT collection.
- **Unfreeze:** Call `asset::unfreeze` with the `asset_id` to restore normal transfer functionality.
:::info
While an asset is frozen, controller transfers are still permitted. This allows issuers or agents to forcibly move tokens or NFTs even when the asset is otherwise locked down.
:::
## Further Reading
- [Fungible Assets](/core/assets/fungible)
- [Non-Fungible Assets](/core/assets/nft)
- [Asset Metadata](/core/assets/metadata)
- [Asset Ownership Transfer](/core/assets/ownership-transfer)
- [Asset Checkpoints](/corporate-actions/checkpoints)
- [Asset Agents & Permissions](/asset-agents)
- [Settlement](/settlement)
- [Corporate Actions](/corporate-actions)
---
:::warning
Anyone can create and configure assets on Polymesh, including setting names, tickers, metadata, and identifiers. The presence of information on-chain does not guarantee that an asset is genuine, regulated, or recognized by any authority. Always perform your own due diligence before interacting with or relying on any asset or its associated data.
:::
Polymesh assets are designed for flexibility, compliance, and extensibility, supporting a wide range of use cases from regulated securities to digital collectibles.
---
## Asset Holdings
## Overview
In Polymesh, all [assets](/core/assets) (excluding the network native token [POLYX](/polyx)) are held at the [identity](/identity) level. This allows Polymesh to enforce [compliance](/compliance) in real time based on claims also held at the identity level.
There are two ways an identity's assets can be held:
- **[Portfolios](/portfolios)**: a logical grouping of assets, identified by a DID and portfolio number. This is the default and most feature-rich holding model, supporting multiple portfolios per identity, custody delegation, and portfolio-level key permissions.
- **Account IDs**: assets held directly against a signing key's account, without an intervening portfolio.
Both holding types are represented on-chain by the `AssetHolder` type (`Account` or `Portfolio(PortfolioId)`), used consistently across the `Asset`, `Nft`, and `Settlement` pallets. Calls that specify where to issue, redeem, or move assets take an `AssetHolderKind` (`Account`, `DefaultPortfolio`, or `UserPortfolio`) to select which of the two — and which portfolio, if applicable — to target.
## Account ID Balances (`AssetHolder::Account`)
Account ID balances let a signing key hold assets directly, identified as an `AssetHolder::Account` rather than an `AssetHolder::Portfolio`. This is useful for simplified transfers to an Account ID without pre-fetching a DID and for workflows that do not require advanced portfolio features.
Key points:
- The balance is associated with a signing key, but compliance still applies based on the key's DID: the chain resolves the account to a DID internally via its key record, and errors if the account isn't linked to any identity. This is not a DID-free holding model, just first-class account-level holding.
- The settlement engine is still used for transfers, and legs can move assets directly to or from an account or a portfolio, in any combination — see [Settlement](/settlement).
- Assets can also move directly between two `AssetHolder`s (accounts or portfolios) via `settlement::transfer_funds`, which usually executes immediately without creating a settlement instruction — see [Direct Transfers](/settlement#direct-transfers-transfer_funds).
- Account ID balances do not support custody — see [Custody Management](/portfolios/custody).
- Account ID balances are not supported by portfolio-level secondary-key permissions.
- Fungible asset holders can grant another account an allowance to spend on their behalf, similar to the ERC-20 `approve`/`transferFrom` pattern — see [Allowances](/core/assets/fungible#allowances). NFTs have no allowance concept.
## Portfolio Balances (`AssetHolder::Portfolio`)
Portfolios allow users to organize their assets underneath their identity, and to flexibly assign key permissions and custody. A particular asset can have different balances across multiple portfolios within the same identity. Portfolios remain the default and most full-featured way to hold assets — see [Portfolios](/portfolios) for portfolio management (creating, renaming, and deleting portfolios, moving funds between them, pre-approving asset receipt) and [Custody Management](/portfolios/custody) for delegating control of a portfolio to another identity.
## AssetHolderKind and AssetHolder
`Asset::issue`/`Asset::redeem` and their NFT equivalents (`issue_nft`/`redeem_nft`) take an `AssetHolderKind` parameter to select whether newly issued or redeemed tokens target the signing key's account or a portfolio under their DID — see [Fungible Assets](/core/assets/fungible) and [Non-Fungible Assets](/core/assets/nft). Settlement legs, and calls like `settlement::transfer_funds`, take an `AssetHolder` for the same purpose — see [Settlement](/settlement).
---
## Compliance
Polymesh enforces compliant asset transfers through on-chain rules based on identity claims. Asset issuers, or their appointed agent, define which claims (such as KYC, accreditation, or jurisdiction) and which claim issuers are required to be present or absent for transfers to be permitted. These rules ensure that only eligible participants can hold or transfer assets, and are checked automatically by the blockchain.
## Overview
Compliance on Polymesh is managed by the on-chain **Compliance Manager** module. Asset issuers (or their agents) can define, update, pause, or reset compliance requirements (rules) for their assets. Each rule specifies which identity [claims](#claims), [claim issuers](#trusted-claim-issuers), and [claim scopes](#claim-scopes) are required for transfers, and can apply to senders, receivers, or both.
Compliance is enforced through a set of requirements (rules) attached to each asset. Each requirement consists of one or more conditions that must be satisfied by the sender, receiver, or both. **If all conditions within at least one rule are satisfied, the transfer is allowed.** Rules are evaluated automatically during every transfer, ensuring that only permitted transactions are executed.
**Key concepts:**
- Multiple **rules** can be defined per asset. Each rule can have multiple sender and receiver conditions.
- Conditions are based on claims held by identities, such as KYC, accreditation, jurisdiction, or custom claims.
- Asset issuers can specify which identities are trusted to issue claims for compliance purposes.
- Rules can specify the required scope for claims (e.g., asset-specific, identity-specific, or custom).
- Claims can have expiry dates, and lockup claims can restrict transfers until a specified time.
- Support for custom claim types and custom claim scopes.
## Claims
Compliance rules are built on top of the Polymesh claims system. **Claims** are attestations made by one identity about another, such as "is accredited" or "is from jurisdiction X." Claims are added on-chain using the `identity::add_claim` extrinsic from the identity pallet.
Claims can be:
- **Standard types:** Accredited, Affiliate, Buy Lockup, Sell Lockup, Know Your Customer, Jurisdiction, Exempted, Blocked, and Custom.
- The `Jurisdiction` claim takes an additional country code parameter, indicating the user's jurisdiction.
- **Custom types:** Asset issuers can define and use custom claim types for specialized compliance needs.
:::info
`CustomerDueDiligence` (CDD) remains available as a legacy claim type in v8. DID registration, not CDD claim presence, is the onboarding gate for identity and asset related activity. This claim type is not typically used in compliance rules.
:::
Each claim has:
- **Scope:** Specifies what the claim applies to (an asset, an identity, or a custom value).
- **Expiry:** (Optional) date after which the claim is no longer valid. When used in conjunction with `Buy Lockup` and `Sell Lockup` claims, an expiry can prevent investors from buying or selling an asset until after the expiry date of the respective claim.
- **Issuer:** The identity that issued the claim.

## Compliance Rules
Compliance rules are structured as follows:
1. **Rules:** Each asset can have multiple rules. At least one rule must be satisfied for a transfer to be allowed (logical OR between rules).
2. **Conditions:** Each rule can have multiple sender and/or receiver conditions. All conditions within a rule must be satisfied (logical AND between conditions).
3. **Condition Types:** Conditions specify the type of claim, the required scope, and (optionally) the trusted claim issuer(s).
:::info
If no compliance requirements are set for an asset, all transfers are allowed by default.
:::
The diagram below illustrates how compliance rules are constructed on Polymesh:

Asset issuers can specify multiple rules for their asset. Each rule includes conditions that either the sender or receiver must satisfy. If the conditions for any rule are met by both parties in a transaction, the transfer leg can proceed.
Each compliance rule's conditions (for both sender and receiver) must be satisfied, although condition lists can be empty. For example, if the sender conditions are blank, all asset holders can send the token without restriction, provided the receiver satisfies the specified receiver conditions.
An asset issuer can also pause compliance on their asset, enabling all transfer legs to execute upon affirmation. Pausing compliance may benefit certain assets, like stablecoins, where the issuer may not wish to impose transfer restrictions. This is managed using the `pause_asset_compliance` and `resume_asset_compliance` extrinsics.
### Condition Types
Each condition within a rule defines a specific requirement (typically [claim](#claims) related) that must be met. The main condition types are:
- **IsPresent:** Requires that a specific claim (and optionally scope and issuer) is present for the sender or receiver.
- **IsAbsent:** Requires that a specific claim is not present.
- **IsAnyOf:** Requires that at least one claim from a specified set is present.
- **IsNoneOf:** Requires that none of a specified set of claims are present.
- **IsIdentity:** Requires that the sender or receiver is a specific identity (by DID) or is an [ExternalAgent](/asset-agents) of the asset. This condition does not rely on claims, but instead checks for a direct match to a specified identity or agent relationship. For example, this can be used to exempt the asset issuer or a designated agent from other transfer requirements.
The **IsIdentity** condition supports two options:
- **Specific:** The sender or receiver must have a specific Decentralized Identifier (DID).
- **ExternalAgent:** The sender or receiver must be an agent of the asset (see [Asset Agents & Permissions](/asset-agents)).
These condition types can be combined within rules to create complex, fine-grained compliance logic.
If any compliance rule is satisfied in a transfer leg, compliance is considered complete, allowing asset issuers to create complex rule sets that ensure asset compliance.
For example, the following rule set requires only a single rule to be satisfied (OR logic between rules):
- Rule 1 - sender does not have a sell lockup scoped to the asset, from the asset issuer; receiver has a KYC claim and is not in the U.S., with claims scoped to the asset or issuer identity and from the issuer's KYC partner.
- Rule 2 - sender does not have a sell lockup scoped to the asset, from the asset issuer; receiver has a KYC claim and an Accredited claim, scoped to the asset or issuer identity and from the issuer's KYC partner.
- Rule 3 - sender's identity is an agent of the asset, with no receiver conditions.
- Rule 4 - no sender restriction; receiver's identity is an agent of the asset.
- Rule 5 - no sender restriction; receiver has an Exempted claim scoped to the asset from the asset issuer.
## Trusted Claim Issuers
While any identity can issue a claim, asset issuers can specify which identities are considered **trusted claim issuers** for compliance purposes. There are two ways to define trusted claim issuers:
- **Default trusted claim issuers:** Set at the asset level, these are used whenever a rule does not specify a claim issuer for a condition. Asset-level defaults are managed using the `complianceManager::add_default_trusted_claim_issuer` and `complianceManager::remove_default_trusted_claim_issuer` extrinsics. When adding a default trusted claim issuer, issuers can specify which claim types the issuer is trusted for, or indicate that the issuer is trusted for all claim types.
- **Rule-defined trusted claim issuers:** Specified directly within a compliance rule condition for a particular claim type or condition. These take precedence over the asset-level defaults for that rule. Rule-defined trusted claim issuers can also be set for specific claim types or for all claim types within the rule, providing granular control over which issuers are accepted for each compliance condition.
Only claims from trusted claim issuers (either default or rule-defined, and for the relevant claim type) will be considered valid for satisfying compliance rules. This allows issuers to:
- Accept claims only from themselves or approved KYC providers
- Restrict certain claim types to specific issuers (e.g., only accept Affiliate claims from the asset issuer)
If a rule does not specify a trusted claim issuer for a claim, the default trusted claim issuers for the asset are used automatically.
## Claim Scopes
Scopes can be considered as a way to group claims. Each claim has a **scope** that determines what it applies to:
- **Asset scope:** The claim is specific to a unique asset ID.
- **Identity scope:** The claim applies to a specific identity.
- **Custom scope:** Arbitrary value for specialized use cases.
Compliance rules can specify which claim scopes are accepted for each condition.
**Examples:**
- A claim scoped to the `ACME` asset UUID (e.g., `12345678-1234-1234-1234-123456123456`) allows the claim to be used with rules that only accept claims scoped to this asset, offering the highest level of control over identity claims.
- A claim scoped to the `ACME Co.` identity (issuer of `ACME-A`, `ACME-B`, and `ACME-C`) enables reuse of identity claims across multiple assets. Here, claim issuers can issue a single KYC claim scoped to the identity rather than multiple asset-specific claims.
- A custom scope such as `ACME Exchange` could apply to claims issued by the ACME Exchange for all onboarded account holders. If an asset issuer lists their token on the ACME Exchange and accepts the exchange as a trusted issuer, the issuer can permit exchange account holders to trade the token by recognizing the exchange's custom scope in compliance rules.
:::info
Identity claim scopes are required to match a condition claim scope to satisfy a condition. However, an Asset type or Identity type claim does not need to match the specific asset or identity involved in the transfer. All scopes should be considered as a way to group claims and can be reused across multiple assets.
:::
:::note
Just because a trusted issuer has issued, for example, a KYC claim on Polymesh, it does not necessarily mean that the KYC was performed to the same requirements as those of a particular asset issuer. Claim issuers may perform KYC for many issuers, each with different standards. Scoping provides a way for issuers to group and recognize only those claims that they are confident meet their own requirements.
:::
## Technical Implementation
Key on-chain extrinsics (dispatchable functions) in the Compliance Manager module include:
- `add_compliance_requirement`: Add a new compliance rule to an asset.
- `remove_compliance_requirement`: Remove a rule from an asset.
- `add_default_trusted_claim_issuer`: Add a default trusted claim issuer for an asset.
- `remove_default_trusted_claim_issuer`: Remove a default trusted claim issuer for an asset.
- `reset_asset_compliance`: Remove all rules for an asset.
- `pause_asset_compliance`: Pause compliance checks for an asset.
- `resume_asset_compliance`: Resume compliance checks for an asset.
- `replace_asset_compliance`: Replace the entire set of compliance rules for an asset (not just a single rule).
- `change_compliance_requirement`: Change the conditions of an existing compliance rule for an asset.
These extrinsics are available via the Polymesh runtime and can be called by asset owners or their appointed agents.
---
## Transfer Restrictions
Polymesh also supports protocol-level transfer restrictions for fungible assets, such as limits on the number of holders, maximum ownership percentages, and claim-based caps. These restrictions are enforced automatically and can be used alongside compliance rules for additional control.
For details and configuration options, see the [Transfer Restrictions](/compliance/transfer-restrictions) page.
---
## Transfer Restrictions
## Overview
Transfer restrictions in Polymesh are implemented using the statistics module, and can only be configured for fungible assets (see the [Fungible Assets](/core/assets/fungible) documentation for more details). The statistics module tracks key metrics ("statistics") about asset holders, and transfer restrictions are enforced based on these statistics at the protocol level. This allows asset issuers to enforce compliance rules such as:
- Limiting the number of unique token holders (count-based restrictions)
- Capping the maximum percentage of the total supply that any single holder can own (percentage-based restrictions)
- Restricting the number of holders or aggregated percentage of the asset held by holders with or without specific claims (claim-based restrictions, e.g., accredited status or jurisdiction; see [Compliance](/compliance/#claims) for more on claims)
Claim-based restrictions can target both holders who possess a claim (e.g., accredited investors) and those who do not (e.g., non-accredited investors), enabling rules such as limiting the number of non-accredited investors by counting holders without the accredited claim. For more on how agent permissions interact with compliance and transfer rules, see [Asset Agents & Permissions](/asset-agents). Venue filtering is also supported as a type of transfer restriction; see [Trading Venues](/settlement/venues).
Restrictions can be applied to all holders of the asset (e.g., total holder count or maximum ownership percentage across the entire supply), or scoped to holders with particular claims (e.g., only those who are accredited investors or belong to a certain jurisdiction). Once configured, transfer restrictions are enforced automatically at the protocol level. Exemptions can be granted to specific identities, allowing them to bypass certain restrictions, but **all holders**, including exempted ones, are still **included in the statistics used for restriction checks and compliance reporting**. This ensures that compliance reporting and enforcement remain accurate and transparent.
:::note
Transfer restrictions can only be configured for fungible assets on Polymesh. Non-fungible assets are not supported by the statistics module.
:::
**Use cases include:**
- **Limiting the number of investors:** Preventing the asset from being held by more than a specified number of unique investors (important for regulatory compliance in some jurisdictions, e.g., US Reg D 12(g) holder limits).
- **Capping individual ownership:** Ensuring that no single investor can accumulate more than a set percentage of the total supply (supports fair distribution and helps prevent monopolies).
- **Enforcing claim-based restrictions:** Restricting the number or percentage of tokens that can be held by investors with specific claims (e.g., only allowing a certain number of non-accredited investors, or capping the percentage of tokens held by investors from a particular country or with a particular status).
- **Dynamic compliance management:** Allowing issuers to update restrictions as regulations or business needs change, and to exempt specific identities (such as company insiders or strategic partners) from certain rules without affecting the overall statistics.
## Restriction Types and Enforcement
The statistics module tracks various statistics about asset holders, and transfer restrictions are enforced based on these statistics. Each type of restriction has specific logic for how it impacts senders and receivers:
:::info Statistic Limit and Performance
A maximum of 4 statistics can be tracked for a single asset at any time. Attempting to add more will result in an error.
Each statistic that is tracked adds additional overhead to every transfer involving the asset, regardless of whether a restriction is enforced on it. For best performance, only enable the minimum set of statistics and restrictions required for your compliance needs.
:::
### Holder Count Restrictions
- Triggered when a new token holder is being added (receiver previously had zero balance).
- Checks if adding the new token holder would exceed the maximum allowed token holder count.
- Does not restrict transfers between existing token holders or when a holder exits completely.
### Holder Percent Ownership Restrictions
- Focuses on the receiver's new total ownership percentage after the transfer.
- Restricts transfers that would cause a receiver to exceed the maximum allowed ownership percentage.
### Claim Count Restrictions
- Enforces both minimum and maximum limits on holders with specific claims.
- If the sender has the relevant claim and is transferring all tokens, ensures the total holders with this claim doesn't drop below the minimum.
- If the receiver has the relevant claim and is a new token holder, ensures the total holders with this claim doesn't exceed the maximum.
- No restriction if the sender/receiver claim status doesn't affect the counts.
### Claim Ownership Percentage Restrictions
- Focuses on the aggregate percentage of the total token supply held by all identities with specific claims (not the percentage of holders with the claim).
- Four cases are handled:
1. Both sender and receiver have the claim: No change in aggregate percentage (allowed).
2. Neither have the claim: No change in percentage (allowed).
3. Only receiver has the claim: Ensures the aggregate percentage doesn't exceed maximum.
4. Only sender has the claim: Ensures the aggregate percentage doesn't drop below minimum.
:::note Behavior When Limits Are Exceeded
There are situations where the configured limits may be temporarily exceeded—for example, when an exempted entity is involved in a transfer, when an investor's claim status changes, or when the asset issuer sets a new limit that is lower than the current number of holders. In these cases, further transfers are only permitted if they do not worsen the violation. Specifically:
- For **count-based restrictions** (e.g., maximum number of holders, claim count): Transfers that would increase the count above the maximum or decrease it below the minimum are blocked. However, transfers that maintain the current count or bring it closer to compliance (such as reducing the number of holders when above the limit) are allowed. For example, if the holder count is already above the maximum, a transfer from one existing holder to another existing holder is allowed, and a transfer where a holder exits completely (transferring all tokens to a new holder) is allowed if the total count does not increase.
- For **percentage-based restrictions** (e.g., maximum ownership percentage, claim ownership percentage):
- For non-claim-based restrictions, any transfer that would cause an individual holder to exceed the maximum is blocked (unless exempt).
- For claim-based restrictions, transfers are blocked if they would cause the aggregate balance of all holders with the claim to exceed the maximum allowed percentage of the total supply. Transfers that do not worsen the violation (such as transferring between existing holders with the claim, or reducing the aggregate claim-holder balance) are allowed.
- **Exemptions** allow certain identities to bypass these restrictions as described in the [Exemptions](#exemptions) section.
In summary, transfers that maintain or improve compliance with the configured limits are permitted, while those that would worsen a violation are blocked (unless an exemption applies).
**Example:**
If the maximum holder count is 100 and there are already 101 holders (perhaps due to a rule change or an exemption), a transfer from Alice (an existing holder) to Bob (a new holder) is only allowed if Alice is exiting completely (so the total count does not increase). Transfers between existing holders are always allowed, regardless of the current count.
:::
These mechanisms ensure that all configured restrictions are enforced at the protocol level, and that compliance is maintained for both count- and percentage-based rules, including those scoped to specific claims or the absence of claims.
## Transfer Restrictions Lifecycle
### 1. Set Active Statistic Types
To begin, use `statistics::setActiveAssetStats` to define which statistics (counts or balances) will be tracked for a given asset. This initializes the storage for the selected stat types. Only identities with Agent permission for the asset can perform this action. Setting new stat types will replace any existing types. You cannot set a new stat type if a previous one is still in use by transfer conditions. Maximum of 4 stat types per asset.
**Parameters:**
- `asset_id`: The asset to configure.
- `stat_types`: The new stat types to track (e.g., `Count` for holder count, `Balance` for percentage-based rules).
- `claimIssuer` (optional): Specify a claim issuer for claim-based stats.
---
### 2. Batch Update Asset Statistics
If you enable a new stat type for an asset that already has holders, or when claims are added or removed from identities, use `statistics::batchUpdateAssetStats` to update the tracked counts or balances. This ensures the statistics reflect the current state of holders and claims. This transaction can be used at any time to synchronize the tracked stats with the actual asset state.
**Parameters:**
- `asset_id`: The asset to update.
- `stat_type`: The stat type to update (must be active for the asset).
- `values`: The set of stat updates to apply (e.g., balances, counts, claim changes).
---
### 3. Set Asset Transfer Compliance
Configure the actual transfer restrictions for the asset using `statistics::setAssetTransferCompliance`. The issuer defines which rules (such as maximum holder count, maximum ownership percentage, or claim-based restrictions) will be enforced. Each restriction must correspond to a stat type that is already active for the asset. Setting new transfer conditions will replace any existing ones. Maximum of 4 transfer rules per asset.
**Parameters:**
- `asset_id`: The asset to configure.
- `transfer_conditions`: The set of transfer conditions to enforce (e.g., `MaxInvestorCount`, `MaxInvestorOwnership`, `ClaimCount`, `ClaimOwnership`).
- `claimIssuer` (optional): Specify a claim issuer for claim-based restrictions.
---
### 4. (Optional) Set Entities Exempt
To exempt specific identities from one or more transfer restrictions, use `statistics::setEntitiesExempt`. Exemptions can be set or removed at any time, and are useful for allowing certain parties (such as company insiders or strategic partners) to bypass specific compliance rules without affecting the overall statistics.
**Parameters:**
- `asset_id`: The asset for which to set exemptions.
- `is_exempt`: Enable or disable exemption for the entities.
- `exempt_key`: The stat type and asset ID for which the exemption applies.
- `entities`: The DIDs of the token holders to set or unset as exempt.
## Exemptions
Exemptions allow specific identities to bypass certain transfer restrictions, but do not remove them from the statistical calculations. The exemption mechanism is applied as follows:
- For **count-based restrictions** (such as maximum number of holders), the **sender** must be exempt to bypass the restriction.
- For **ownership percentage restrictions** (such as maximum percentage ownership), the **receiver** must be exempt to bypass the restriction.
Exemptions are only checked if a transfer would otherwise be blocked by a restriction. If the transfer does not violate any restriction, exemptions are not considered.
All identities, including those that are exempt, are still included in the statistics used for compliance checks and reporting. This ensures that the asset's compliance status and reporting remain accurate and complete, regardless of exemptions.
---
## Trading Venues
## Overview
Venues on Polymesh provide a logical grouping of settlement instructions. They provide a way to organize and control trading activities, with specific venues being used for particular types of transactions such as primary issuance, exchange trading, or fundraising events.
## Venue Types and Use Cases
Venues can be categorized into different types based on their intended use:
- **Distribution**: Used for primary issuance and initial distribution of assets to investors
- **Exchange**: Designated for exchange-based trading, allowing organized secondary market trading
- **STO**: Specifically for Security Token Offerings or fundraising events, managing token sales
- **Other**: General purpose venue type for specialized use cases
### Common Applications
- **Primary Issuance**: Manage initial token distribution to investors through a controlled venue
- **Exchange Integration**: Create dedicated venues for specific trading platforms or exchanges
- **Fundraising**: Organize and control token sales through STO venues
- **Compliance Management**: Restrict trading to specific approved venues for regulatory compliance
- **Settlement Tracking**: Group related transactions for better organization and audit trails
:::info
Venues are optional for settlement instructions. When no venue is specified, the instruction will use a default global venue. However, some assets may be restricted to trading only on specific venues as part of their compliance rules. A venue is required for instructions with an off chain leg as receipts for off chain legs must be signed by a signatory authorized by the venue owner.
:::
## Venue Management
Each venue has an owner identity that:
- Is the sole identity that can create settlement instructions within the venue
- Can execute fully affirmed instructions associated with that venue
- Can add or remove receipt signers for off chain legs
Once an instruction has been created inside a venue, it cannot be moved to another venue. Venues can be reused at any time.
## Technical Implementation
### Creating Venues
A venue can be created by calling `settlement::create_venue` with the following parameters:
- `details`: Venue details (such as a descriptive name)
- `signers`: List of authorized receipt signers (for off chain leg receipts)
- `typ`: Venue type (Distribution, Exchange, STO, or Other)
Upon successful creation, the venue is assigned a unique Venue ID that can be used for creating and approving instructions within that venue. The venue owner can later modify venue properties using:
- `settlement::update_venue_details` to update the venue's details
- `settlement::update_venue_type` to change the venue type
- `settlement::set_venue_signers` to update the list of authorized receipt signers
### Venue Filtering
Asset issuers have granular control over where their assets can be traded through venue filtering transfer restrictions:
#### Global Venue
When no specific venue is provided for a settlement instruction, it automatically uses a global default venue. This makes venue specification optional for basic transfers while still maintaining the ability to track and manage settlements.
#### Asset-Level Venue Restrictions
Asset issuers can implement venue-based trading restrictions using the following chain methods:
1. **Enabling Venue Filtering**: Use `settlement::set_venue_filtering` to activate venue restrictions for the asset
2. **Managing Allowed Venues**: Use `settlement::allow_venues` and `settlement::disallow_venues` to maintain an allowlist of permitted venues
When venue filtering is enabled for an asset, it works on an allowlist basis:
- Only venues that have been explicitly allowed through `allow_venues` can be used for trading that asset
- Any venue not in the allowlist will be rejected for settlement instructions involving the asset
- The global default venue cannot be used while filtering is enabled
- If venue filtering is enabled but no venues are in the allowlist, no trading can occur
These restrictions provide issuers with:
- Control over where their assets can be traded
- Ability to ensure trading only occurs on approved or regulated venues
- Flexibility to adjust trading permissions as needed
:::note
Venue filtering is independent of other compliance rules and transfer restrictions. Assets can have both venue restrictions and other compliance rules and transfer restrictions active simultaneously.
:::
---
## Settlement Mediators
## Overview
Settlement mediators in Polymesh are entities that must affirm settlement instructions before they can be executed. They provide an optional additional layer of control and validation in the settlement process, enabling scenarios where third-party verification or approval is required before transfers can execute on chain. Mediators can represent transfer agents, smart contracts, or other entities that need to validate transactions before they occur.
Mediator affirmations can have an optional expiry after which it will no longer be valid requiring the mediator to reaffirm the instruction before it can execute. Mediators can also withdraw their affirmation or reject/cancel an instruction entirely.
## Mediator Types and Scope
Mediation can be enforced at two levels:
- **Asset Level**: Required for all settlements involving specific assets (set by asset issuers)
- **Instruction Level**: Required for specific settlement instructions (chosen during instruction creation)
### Asset-Level Mediators
Asset issuers can designate mediators that must approve all transfers of their assets, ensuring consistent validation criteria regardless of who creates settlement instructions.
**Management**: Asset mediators are controlled through `asset::add_mandatory_mediator` and `asset::remove_mandatory_mediator` transactions.
**Behavior**:
- All settlement instructions involving the asset require mediator approval
- Multiple mediators can be assigned to a single asset
- Applies automatically to any instruction involving the asset
### Instruction-Level Mediators
Settlement instructions can specify additional mediators beyond any asset-level requirements, allowing flexible validation for specific transaction needs.
**Scope**: These mediators are instruction-specific and must approve in addition to any asset-level mediators.
## Mediator Functions and Use Cases
Mediators enable various validation scenarios:
- **Transfer Agent Oversight**: Ensuring proper validation by authorized agents
- **Regulatory Compliance**: Third-party compliance verification before execution
- **Smart Contract Validation**: Programmatic verification of settlement conditions
- **Multi-Party Authorization**: Complex workflows requiring multiple approvals
- **Cross-Chain Coordination**: Validating external blockchain conditions before settlement
## Mediator Actions and Workflow
Mediators have several control mechanisms during the settlement process:
**Required Actions:**
- `settlement::affirm_instruction_as_mediator` - Provide affirmation (with optional expiry)
- Must affirm before instruction can execute (like all other parties)
**Optional Control Actions:**
- `settlement::reject_instruction_as_mediator` - cancels the instruction entirely if called before executing
- `settlement::lock_instruction` - Lock instruction for execution (SettleAfterLock only)
- `settlement::unlock_instruction` - Explicitly unlock a locked instruction, starting the relock cooldown (SettleAfterLock only)
**Affirmation Rules:**
- Mediators can set expiry times for their affirmations
- Expired affirmations become invalid and require renewal
- All mediators (asset-level and instruction-level) must affirm before execution
- If a mediator is also a counterparty, they need separate affirmations for both roles
## Settlement Locking (SettleAfterLock Instructions)
Settlement locking is a specialized workflow designed for scenarios where additional validation or cross-chain/off-chain coordination is required before final settlement. This feature prevents parties from withdrawing affirmations or rejecting instructions once the locking process begins.
### Use Cases
Settlement locking is particularly valuable for:
- **Cross-Chain Settlements**: Ensuring Polymesh instructions cannot be cancelled after corresponding actions have been initiated on other blockchains
- **Off-Chain Coordination**: Preventing instruction cancellation after external systems have begun processing related transactions
- **Multi-Stage Workflows**: Securing instructions that depend on sequential external validations or approvals
- **Regulatory Escrow**: Creating time-locked periods where instructions cannot be modified during compliance verification
### How Settlement Locking Works
1. **Instruction Creation**: An instruction must be created with `SettlementType::SettleAfterLock`
2. **Standard Affirmation Process**: All parties and mediators affirm the instruction normally
3. **Locking Phase**: A mediator calls `settlement::lock_instruction` to lock the instruction for execution
- **Required Parameters**: The call requires both the `instruction_id` to specify which instruction to lock and a `weightLimit` parameter
- **Weight Mechanism**: Weight in Substrate chains acts similar to gas in Ethereum - it measures computational cost and prevents infinite loops. If the weight limit is too low, the transaction will fail before execution begins
- **Runtime API Available**: Use the `settlement::lock_instruction_weight` runtime API to get the correct weight for a specific instruction and ensure proper fee calculation
- **Comprehensive validation occurs**: All settlement conditions are validated including compliance rules, mediator affirmation expiries, asset availability, and venue permissions
- **Execution guarantee**: Once successfully locked, execution by any mediator cannot fail - all potential failure conditions have been pre-validated
- Assets are locked and the instruction status changes to `LockedForExecution`
- A timestamp is recorded for the locking period
4. **Protected Period**: During the lock period:
- Parties cannot withdraw affirmations or reject the instruction
- The instruction is protected from cancellation
- Assets remain locked until settlement or lock expiry
5. **Final Settlement**: The instruction can be settled using standard settlement methods during the lock period
### Lock Period, Unlocking, and Relocking
- **Maximum Lock Period**:
- **Mainnet and Testnet**: 24 hours (86,400,000 milliseconds)
- **Development chains**: 24 minutes (1,440,000 milliseconds)
- **Explicit Unlock**: A mediator can call `settlement::unlock_instruction` to unlock a `LockedForExecution` instruction before its lock period expires. This starts a **relock cooldown** during which the instruction cannot be relocked:
- **Mainnet and Testnet**: 4 hours (14,400,000 milliseconds)
- **Development chains**: 10 minutes (600,000 milliseconds)
- **Lock Expiry Behavior**: When the maximum lock period elapses without an explicit unlock:
- The instruction status remains `LockedForExecution`
- **Execution is blocked** unless a mediator successfully relocks the instruction (see below)
- **Assets remain locked** and are NOT automatically released
- **Withdrawal of affirmations remains blocked** (status is still `LockedForExecution`)
- Rejection by any valid party is allowed after expiry
- Assets are only released by execution, rejection, or successful relocking followed by execution
- **Relocking**: A mediator can call `lock_instruction` again on an already-locked or previously-unlocked instruction, subject to two independent conditions, either of which permits the relock:
- If the instruction is still `LockedForExecution` and its maximum lock period **plus** the relock cooldown has elapsed since it was originally locked, or
- If the instruction was explicitly unlocked via `unlock_instruction` and the relock cooldown has elapsed since that unlock
- Calling `lock_instruction` on a still-locked instruction before this window opens fails with `InstructionAlreadyLocked`; calling it during an active cooldown after an explicit unlock fails with `RelockCooldownNotExpired`
- Each successful relock increments a per-instruction relock counter. Relocking is capped at **3 relocks** per instruction (`MaxRelockCount`); a further attempt fails with `MaxRelockCountExceeded`
- **Timestamp Tracking**: The system tracks both when an instruction was locked and, separately, when it was last explicitly unlocked, to enforce the windows above.
### Requirements and Restrictions
**Who Can Lock, Unlock, or Relock Instructions:**
Only designated mediators can call `settlement::lock_instruction` or `settlement::unlock_instruction`. Mediators can be designated:
- At the instruction level (specified during creation)
- At the asset level (mandatory mediators for involved assets)
**Technical Requirements:**
- Instruction must use `SettlementType::SettleAfterLock`
- All standard settlement conditions must be met (affirmations, compliance, etc.) when locking or relocking
- Caller must be an authorized mediator
- Relocking is subject to the cooldown and relock-count limits described above
- **Required Parameters** (`lock_instruction`):
- `instruction_id`: The ID of the instruction to lock
- `weightLimit`: Weight allocation with `refTime` (computational time) and `proofSize` (storage proof size) values. Use the `settlement::lock_instruction_weight` runtime api call to determine the correct weight
- **Required Parameters** (`unlock_instruction`):
- `instruction_id`: The ID of the instruction to unlock
**Compliance and Validation:**
:::info Critical Design Feature
When locking an instruction, **all compliance rules, mediator affirmation expiries, asset availability, and settlement conditions are validated**. Once successfully locked, execution by any mediator is guaranteed to succeed - no compliance or validation failures can occur during execution.
This design ensures that cross-chain coordinators can rely on Polymesh settlement completion once locking succeeds, enabling atomic cross-chain operations.
:::
**During Lock Period:**
- **Execution**: Only mediators can execute the instruction during the lock period
- **Rejection**: Only mediators can reject the instruction during the lock period (unless lock period has expired)
- **Withdrawal of affirmations**: Prevented for all parties during lock period
- **Affirmation**: Standard affirmation rules apply
- **Unlocking**: Any mediator can unlock the instruction early via `unlock_instruction`, which starts the relock cooldown
- **Multiple mediators**: All mediators have equal powers to execute, reject, unlock, or relock
**After Lock Expiry:**
- **Execution**: Blocked unless a mediator successfully relocks the instruction (subject to the cooldown and relock-count limits described above)
- **Rejection**: Any valid party can reject the instruction
- **Withdrawal of affirmations**: Still blocked (instruction status remains `LockedForExecution`)
- **Re-locking**: Mediators can relock the instruction once the maximum lock period plus the relock cooldown has elapsed, up to `MaxRelockCount` (3) times total
### Cross-Chain Settlement Use Cases
SettleAfterLock instructions are primarily designed for **cross-chain settlement coordination**. Common scenarios include:
**Cross-Chain Service Providers:**
1. User creates SettleAfterLock instruction on Polymesh with a cross-chain service as a mediator
2. Cross-chain service validates both chains are ready
3. Service affirms the instruction and calls lock_instruction when cross-chain conditions are met
4. Settlement can be executed by any mediator during the lock period
5. If cross-chain coordination fails, the service can reject during lock period
**Multi-Chain Atomic Swaps:**
- Coordinate simultaneous settlement across multiple blockchain networks
- Ensure atomic execution or failure across all participating chains
- Prevent partial settlement when cross-chain conditions aren't met
**External System Integration:**
- Allow external systems to validate off-chain conditions before settlement
- Coordinate with traditional financial systems or other blockchain networks
- Implement complex business logic that requires external validation
**Instructions Without Explicit Mediators:**
SettleAfterLock instructions require mediators to be lockable. Without mediators at either instruction or asset level, the instruction behaves like standard `SettleManual` settlement. This ensures:
- Cross-chain coordination requires explicit mediator designation
- Asset-level policies can enforce locking requirements
- Instructions default to standard settlement when no coordination is needed
:::note Dual Role Requirements
If a mediator is also a counterparty, they must provide separate affirmations:
- Mediator affirmation: `affirm_instruction_as_mediator`
- Counterparty affirmation: Standard instruction affirmation for their leg(s)
Both are required before the instruction can settle.
:::
:::info
Mediators provide flexible transfer control and validation mechanisms. They integrate with other Polymesh features like venues and compliance rules to create comprehensive settlement governance frameworks.
:::
---
## Off-Chain Settlement
## Overview
Off-chain settlement legs in Polymesh enable the representation and verification of asset transfers that occur outside the blockchain. This can include traditional financial transactions like bank transfers, physical asset deliveries, transfers on other blockchain networks, or cross-chain settlement processes. This mechanism is essential for integrating both traditional financial infrastructure and other digital systems with Polymesh's on-chain settlement process.
Each off-chain leg requires a receipt signed by an authorized venue signer, providing a cryptographically signed attestation that the external transfer has occurred. While this is not a cryptographic proof of the transfer itself, it represents a trusted party's confirmation that the off-chain transfer has been completed.
## Common Use Cases
Off-chain settlement legs and receipts enable various integration scenarios:
- **Traditional Banking**: Coordinating bank wire transfers or other fiat currency payments
- **Physical Settlement**: Managing delivery of physical assets, securities certificates, or commodities
- **Payment Systems**: Integration with traditional payment networks or financial messaging systems
- **Cross-Chain Settlement**: Coordinating settlements across multiple blockchain networks where assets on different chains are exchanged
- **Cross-Chain Bridges**: Facilitating asset transfers or wrapped token minting/burning between networks
- **External Trading Platforms**: Integrating with existing trading or settlement systems
## Off-Chain Legs
An off-chain leg represents any transfer or payment that happens outside of Polymesh. Common examples include:
- Bank wire transfers or ACH payments
- Cash payments or deposits
- Physical asset or certificate deliveries
- Credit card or payment processor transactions
- Cross-chain settlements and token transfers
- Bridge protocol operations
When creating a settlement instruction that includes off-chain legs, each leg must specify:
- The [venue](/settlement/venues) that will validate the transfer
- The parties involved (sender and receiver)
- The asset and amount being transferred
- Any additional metadata about the transfer
:::note
Settlement instructions containing off-chain legs must be associated with a specific [venue](/settlement/venues) - they cannot use the default global venue. This ensures proper authorization and receipt validation.
:::
## Receipt Signers
Receipt signers are keys authorized by a venue owner to validate off-chain transfers by providing signed attestations. They serve several key functions:
- **Transfer Validation**: Verifying and attesting that off-chain transfers have occurred
- **Receipt Generation**: Creating signed attestations of transfer completion
- **Settlement Integration**: Bridging external systems with on-chain settlement
### Managing Receipt Signers
Venue owners can manage receipt signers using:
- `settlement::set_venue_signers`: Add or remove authorized receipt signers for a venue
- A signing key does not need to be associated with a Polymesh identity however the cryptographic proof generated by the signer must be submitted by a key with a valid Polymesh Identity.
- Multiple signers can be authorized for a single venue
## Technical Details
### Receipt Requirements
For an off-chain leg to be considered complete:
- The receipt must be signed by a currently authorized signer for the venue
- The signature must be cryptographically valid
- The receipt must correctly reference the instruction and leg IDs
- The receipt cannot be withdrawn once submitted
:::note
If a venue's authorized signers are changed, previously submitted receipts remain valid as long as they were signed by an authorized signer at the time of submission.
:::
### Receipt Generation Process
When generating an off-chain receipt, the following information must be encoded and signed:
1. **Required Receipt Data**:
- The unique receipt ID (UID)
- The instruction ID
- The leg ID within the instruction
- The sender's identity (DID)
- The receiver's identity (DID)
- The asset ticker symbol/identifier
- The transfer amount
2. **Signature Generation**:
- The data elements are concatenated in a deterministic order
- Each element is hex-encoded
- The concatenated hex string is wrapped with `` and `` tags to form the final payload
- An authorized venue signer creates a cryptographic signature of this wrapped payload
- The signature can be generated using supported key types (SR25519, ED25519, ECDSA)
3. **Receipt Assembly**:
- The final receipt combines:
- The receipt UID
- The instruction ID
- The leg ID
- The signer's information
- The generated signature
- Optional metadata about the transfer
:::note
The exact data encoding and signature generation process must follow the chain's requirements to ensure the receipt is valid when submitted. The payload data must be wrapped with `` and `` tags before signing. Tools such as the Polymesh SDK provide helper functions to generate valid receipts with the correct format.
:::
### Receipt Process
The off-chain settlement process follows these steps:
1. **Instruction Creation**: A settlement instruction is created with one or more off-chain legs
2. **Off-Chain Transfer**: The actual transfer occurs in the external system
3. **Receipt Generation**: An authorized venue signer creates a receipt as detailed above.
:::note
Each receipt requires a unique ID (UID) that is tracked via the `settlement::ReceiptsUsed` storage. This ensures receipts cannot be reused across different legs or instructions.
:::
4. **Receipt Submission and Affirmations**:
- Any party can submit the signed receipt using `settlement::affirm_with_receipts`
- If the receipt submitter is also a party to other legs they can simultaneously affirm those legs by using `settlement::affirm_with_receipts_with_count`
- The receipt's signature and UID are verified:
- Signature must be from a currently authorized signer for the venue
- UID must not have been used in any other receipt (tracked via `settlement::ReceiptsUsed` storage)
- The receipt cannot be modified once submitted
- Affirmations for other legs in the instruction can occur independently
- Parties can provide their affirmations either before or after the receipt is submitted
5. **Settlement Completion**: The instruction can settle once both conditions are met:
- All required receipts for off-chain legs are submitted
- All required affirmations from instruction parties are in place
:::info
Receipts can be submitted by anyone with a valid signed receipt - not just the venue owner or signers. This allows for flexible integration with external systems while maintaining security through cryptographic signatures. The order of receipt submission and leg affirmations does not matter - parties can affirm their legs before or after receipts are submitted.
:::
:::info
The combination of venue signers, cryptographic receipts, and on-chain verification creates a secure and flexible system for integrating external transfers with Polymesh's settlement process.
:::
---
## Settlement
## Overview
Settlement in Polymesh is the process by which assets are transferred between identities in a regulated and compliant fashion. Asset balances can be held in [portfolios or directly on signing accounts](/asset-holdings) linked to an identity. It requires all counterparties to affirm (agree) an instruction (a set of asset transfers) before the instruction settles (completes).
Key characteristics of Polymesh settlement:
- **Multi-party coordination**: All counterparties must explicitly authorize transfers
- **Double spending prevention**: Assets are locked upon sender affirmation
- **Flexible execution**: Multiple settlement types accommodate different workflow requirements
- **Unilateral rejection**: Any counterparty can reject an instruction at any time prior to execution
- **Optional mediation**: Third-party validation through [settlement mediators](/settlement/mediators) (e.g. a transfer agent)
## Settlement Types
Instructions can be configured with different settlement types to accommodate various workflow requirements:
- **SettleOnAffirmation**: Settles automatically when all parties affirm (most common)
- **SettleOnBlock**: Settles at a specific block number defined when the instruction is created
- **SettleManual**: Requires manual execution after all affirmations and after a specified block
- **SettleAfterLock**: Requires [mediator locking](/settlement/mediators#settlement-locking-settleafterlock-instructions) before settlement execution
### Instruction Dates
All instruction types support optional metadata dates:
- **Trade Date**: Indicates when the instruction becomes valid for interaction (informational only)
- **Value Date**: Indicates when the instruction should ideally be settled (informational only)
:::important Date Enforcement
These date fields are purely informational metadata and are **not enforced** by the Polymesh protocol. The only validation is that `value_date` must be greater than or equal to `trade_date` when both are specified.
:::
## Asset Movement Options
Instructions can settle via:
- **On-chain movement**: Direct transfer of on-chain assets between any combination of portfolios and Account IDs.
- **Off-chain payment receipts**: Signed attestations that off-chain asset transfers occurred. See [Off-Chain Settlement](/settlement/off-chain/) for details.
## Double Spending Prevention
Polymesh prevents double spending through immediate asset locking:
- When a sender affirms an instruction, required assets are immediately locked (whether held in a portfolio or at an Account ID)
- Locked assets cannot be used in other transactions until released
- Asset locks are only released when:
- The instruction is successfully executed (assets are transferred)
- The instruction is explicitly rejected by any counterparty (`reject_instruction` unaffirms and unlocks all counterparty legs, including ones previously affirmed)
- The asset owner maintains beneficial ownership even when locked
:::note Asset Locks vs Mediator Locks
Asset locks prevent double spending by locking sender assets upon affirmation. This is separate from [mediator locks](/settlement/mediators#settlement-locking-settleafterlock-instructions) used in `SettleAfterLock` instructions, which control execution timing.
:::
## Roles and Permissions
### Asset Issuer
- Controls which identities can create instructions referencing their asset
- Can designate mandatory mediators for all transfers of their asset
- May leave instruction creation unrestricted
Asset issuers may assign permissions to another identity as an agent of the asset to manage these functions on their behalf.
### Venues
- Optional organizational structure for grouping related instructions
- Only venue creators can create instructions within their venue
- Venue creators can cancel pending instructions within their venue
- Required for instructions with off-chain legs (to define authorized signatories)
- Control authorized signatories for payment receipt validation
For more details on venues, see [Settlement Venues](/settlement/venues).
### Counterparties
- Any identity referenced as a payer/receiver in an instruction leg
- Must authorize instructions where they are a counterparty before settlement execution
- Can reject instructions unilaterally
- Assets are locked upon affirmation to prevent double spending
### Mediators
Optional third-party entities that provide additional validation and control. See [Settlement Mediators](/settlement/mediators) for detailed information.
## Affirmation Requirements
By default all instruction legs require explicit affirmations from all counterparties before settlement. This requirement has important implications:
- **No airdropping**: Assets cannot be sent to recipients without their explicit consent, providing multiple protections:
- **Legal and tax protection**: Prevents unwanted taxable events, regulatory reporting requirements, or legal liabilities from unsolicited asset transfers
- **Security and reputation**: Avoids accounts being flagged for handling assets linked to hacks, sanctions, or other compliance concerns
- **Spam and scam prevention**: Blocks unsolicited airdrops that may be phishing attempts, scams, or wallet pollution
- **Operational clarity**: Maintains clean, auditable portfolios without unwanted tokens that could create confusion or management overhead
- **Compliance benefits**: Creates a clear, auditable trail of consent and acknowledgment, simplifying regulatory reporting and reducing legal ambiguity
- **Custodian control**: Entities such as custodians have full control over what assets they receive and when they take custody, enabling proper due diligence and risk management
- **Three-step processes**: The affirmation requirement enables on-chain replication of traditional financial three-step settlement processes (instruction → affirmation → execution)
While this affirmation process increases friction compared to simple transfers, it provides essential control and compliance capabilities for regulated financial markets.
### Asset Exemptions and Pre-Affirmation
For certain types of assets or workflows, the default affirmation requirement may not be practical. Polymesh provides several mechanisms to streamline settlement by allowing exemptions or pre-affirmations to receive an asset. The ability to exempt affirmation for certain assets helps reduce friction when not appropriate, but requires explicit opt-in in most cases:
#### Identity-Level Asset Exemptions
Identities can exempt themselves from requiring affirmations when receiving specific assets:
- **Pre-approval**: An identity can pre-approve specific Asset IDs, eliminating the need for explicit affirmations on incoming transfers
- **Identity control**: Each identity manages their own exemption list independently
- **Asset-specific**: Exemptions are granted per asset, allowing granular control
- **Revocable**: Exemptions can be added or removed at any time
This is particularly useful for:
- Stable coins and other high-frequency transfer assets
- Institutional portfolios receiving regular distributions
- Service provider identities that handle many incoming transfers
#### Global Asset Exemptions
The Polymesh governing council can designate certain Asset IDs as globally exempt from requiring affirmations on receipt. This applies network-wide and is typically reserved for:
- Widely-used stable coins or utility tokens
- Assets where universal pre-approval enhances network efficiency
#### Portfolio-Level Exemptions
In addition to identity-level exemptions, portfolio level exemptions can be set. See the [Portfolios](/portfolios/#preapprove-receiving-an-asset-in-a-portfolio) section for further details..
:::note Exemption Scope
Asset exemptions only apply to **receiving** transfers. Sending transfers always require explicit authorization from the asset owner or their authorized custodian, regardless of exemption status.
:::
## Instruction Structure
Settlement instructions are composed of one or more **legs**, each representing a single asset transfer between portfolios.
### Instruction Components
An instruction contains:
- **Settlement type**: Determines execution behavior (SettleOnAffirmation, SettleOnBlock, etc.)
- **One or more legs**: The actual asset transfers to be executed
- **Optional venue**: Organizational grouping and signatory control for off-chain legs (see [Settlement Venues](/settlement/venues))
- **Optional mediators**: Third-party validation entities (see [Settlement Mediators](/settlement/mediators))
- **Optional dates**: Trade date and value date metadata
- **Optional memo**: Descriptive publicly visible text for the instruction
### Leg Components
Each leg within an instruction specifies:
- **Asset**: The specific asset to be transferred (identified by Asset ID)
- **Amount**: The quantity of the asset to transfer (for fungible assets)
- **NFTs**: The specific NFT IDs to transfer (for non-fungible assets, up to 10 NFTs per leg)
- **Sender**: The portfolio or Account ID from which assets will be transferred
- **Receiver**: The portfolio or Account ID that will receive the assets
- **Leg type**: On-chain transfer or off-chain receipt
### Leg Types
**Fungible legs**:
- Transfer a specific quantity of fungible tokens between any combination of portfolios and Account IDs
- Assets are locked when the sender affirms and transferred upon execution
- Most common type for security tokens and other divisible assets
**Non-fungible legs**:
- Transfer specific NFT IDs from a single collection between any combination of portfolios and Account IDs
- Can transfer up to 10 individual NFTs per leg from the same collection
- Each NFT is identified by its unique NFT ID within the collection
- Assets are locked when the sender affirms and transferred upon execution
**Off-chain legs**:
- Represent transfers that occur outside Polymesh (e.g., fiat currency, commodities)
- Require a signed payment receipt as proof of transfer
- Must be associated with a venue that defines authorized signatories
For detailed information on off-chain settlement, see [Off-Chain Settlement](/settlement/off-chain/).
### Multi-Leg Instructions
Instructions can contain multiple legs to represent complex transactions:
- **Atomic execution**: All legs execute together or none execute at all
- **Cross-leg dependencies**: One leg's failure causes the entire instruction to fail
- **Mixed asset types**: Instructions can combine fungible, non-fungible, and off-chain legs
- **Portfolio coordination**: Multiple portfolios can be involved across different legs
- **Atomic DvP settlement**: Enable on-chain delivery versus payment transactions where asset transfers and payments settle simultaneously
## Instruction Lifecycle
### Status States
Instructions progress through several status states:
- **Pending**: Created and awaiting affirmations or execution
- **Failed**: Execution was attempted but failed due to constraints
- **Success**: Executed successfully with assets transferred
- **Rejected**: Explicitly rejected by a counterparty
- **LockedForExecution**: Locked by a mediator awaiting execution (`SettleAfterLock` only)
### Basic Workflow
1. **Creation**: Instruction created with specified legs, settlement type, and optional mediators
2. **Affirmation**: Counterparties (and mediators if required) affirm the instruction (assets locked for senders). Where [portfolio custody](/portfolios/custody/) has been assigned, the controller of that portfolio is responsible for affirmation rather than the portfolio owner.
3. **Execution**: Instruction settles based on its settlement type
4. **Completion**: Assets transferred and instruction marked as successful
## Portfolio and Account ID Transfer Path
Settlement supports transfers between portfolios and Account IDs (signing key balances) in any combination, aligning with traditional key based balance models of other chains while still leveraging the settlement engine for compliance and regulatory requirements. The settlement engine handles all transfers between portfolios and Account IDs, ensuring consistent compliance checks, double-spend prevention, and regulatory controls regardless of the transfer path.
Both holding types are represented by the `AssetHolder` type (`Account` or `Portfolio`), used throughout the `Asset`, `Nft`, and `Settlement` pallets. Settlement instructions still require the sender and receiver's DIDs to be resolvable on-chain: an `AssetHolder::Account` is looked up against the account's identity key record, and the call fails if the account isn't linked to any identity.
## Account ID Transfers (`transfer_asset`)
`asset::transfer_asset` is a simplified single-leg fungible asset transfer API that accepts a receiver's Account ID directly, eliminating the need for clients to pre-resolve the receiver's DID. The chain resolves the receiver's DID internally via on-chain identity lookup for compliance checks. This creates a streamlined Account ID to Account ID transfer model while still leveraging the settlement engine under the hood.
:::info
The `asset::transfer_asset` function is designed for simple single-leg transfers between Account IDs. If a sender only holds an asset in a [Portfolio](/portfolios/) they must first [move](/portfolios/#moving-funds-between-portfolios) the asset to their Account ID using `portfolio::move_portfolio_funds` before calling `asset::transfer_asset`.
:::
### How Account ID Transfers Work
Account ID transfers work with balances held directly at the signing key's Account ID (`AssetHolder::Account` rather than an `AssetHolder::Portfolio`):
1. **Sender initiates transfer**: Sender calls `transfer_asset` with:
- Receiver's Account ID (public address)
- Asset ID
- Amount
- Optional memo
2. **On-chain DID resolution**: The chain automatically resolves the sender and receiver's DID from their Account IDs for compliance checks, removing any client-side lookups
3. **Instruction creation**: A settlement [instruction](#instruction-structure) is created with a single leg:
- From: Sender's Account ID
- To: Receiver's Account ID
- Asset: The specified asset ID
- Amount: The transfer quantity
4. **Sender affirmation**: The sending account is automatically [affirmed](#affirmation-requirements) and assets are locked
5. **Settlement happens based on receiver status**:
- **If receiver [pre-approved](#asset-exemptions-and-pre-affirmation) the asset**: Instruction executes immediately in the same block without pending affirmation.
- **If receiver has NOT pre-approved**: Instruction created with a pending status and requires the receiver to call `asset::receiver_affirm_asset_transfer` or affirm via the settlement engine. This call also automatically resolves the receiver's DID and attempts instruction execution in the same block.
- Receiver can alternatively reject with `asset::reject_asset_transfer`
### Key Characteristics
- **Single-leg only**: Account ID transfers cannot have multiple legs (no multi-leg atomicity)
- **No explicit portfolio support**: Transfers work directly with the signing key's Account ID balance
- **Auto-execution on receiver approval**: When receiver affirms, execution happens atomically in the same transaction - no scheduler delay, immediate feedback on success or failure.
- **Same engine**: Despite simplified API, uses the full settlement engine for compliance checks, double-spend prevention, and regulatory requirements
By default, receiver affirmation is automatic in the model above. Receivers can opt into requiring explicit affirmation for their account by calling `settlement::set_mandatory_receiver_affirmation`.
## Direct Transfers (`transfer_funds`)
`settlement::transfer_funds(from: Option, to: AssetHolder, fund: Fund)` moves a fungible amount or a set of NFTs directly between any two `AssetHolder`s — accounts or portfolios, in any combination. If nothing requires the receiver to affirm — no [mandatory receiver affirmation](#account-id-transfers-transfer_asset) set for the receiving account and no mandatory mediators on the asset — the transfer executes immediately in the same call, either succeeding or failing right away with no scheduler delay and no separate manual execution step, and a successful transfer emits `FundsTransferred(IdentityId, AssetHolder, AssetHolder, Fund)`. If the receiver requires explicit affirmation, or the asset has mandatory mediators, `transfer_funds` instead creates a settlement instruction behind the scenes and follows the normal [affirmation](#affirmation-requirements) and [execution](#execution-behavior) flow for it.
If the caller is not the owner of an account-based `from`, `transfer_funds` draws down an allowance previously granted via `asset::approve` instead of requiring the owner's signature on the transfer itself — see [Allowances](/core/assets/fungible#allowances). This is the mechanism a smart contract or other third party uses to move a pre-approved amount of a user's tokens without that user co-signing every transfer. NFTs have no allowance concept, so a third-party `transfer_funds` call for an NFT `fund` fails.
### NFT Transfers (`transfer_nft`)
`nft::transfer_nft(nfts, to, memo)` is the NFT equivalent of `transfer_asset`: a direct, account-to-account transfer of specific NFTs from a single collection, with no settlement instruction and no affirm/reject cycle — it completes immediately in the same call. Unlike `transfer_asset`, it does not accept a portfolio on either side; use `transfer_funds` instead if a portfolio is involved. See [Non-Fungible Assets](/core/assets/nft#nfts-and-the-settlement-pallet) for the full call details.
## Execution Behavior
Settlement instructions execute differently based on their settlement type:
### Automatic Execution (SettleOnAffirmation & SettleOnBlock)
**SettleOnAffirmation**: When the final required affirmation is received, the instruction is automatically **scheduled** for execution. The affirmation transaction succeeding only confirms scheduling - actual settlement occurs in a subsequent block. While this is typically in the next block execution is not guaranteed to be immediate if the chain is congested.
**SettleOnBlock**: Instruction is scheduled for execution at the specified block number. If affirmations are still missing at the target block, the instruction fails permanently.
Both types provide limited immediate feedback since execution is deferred to later blocks.
### Manual Execution (SettleManual & Failed Instructions)
Manual execution provides immediate feedback by attempting settlement within the same transaction:
- Success or failure is reported instantly
- If execution fails, the transaction reverts with clear error information
- Can be used to retry failed instructions once underlying issues are resolved
- Works for any instruction type that has all required affirmations
- **Batched operations**: Affirmation and execution can be combined in a single transaction for atomic settlement with immediate feedback
### Mediated Execution (SettleAfterLock)
See [Settlement Mediators](/settlement/mediators) for detailed workflow information on mediator-controlled execution.
## Failure Recovery
When instructions fail during execution:
1. **Asset locks remain**: Failed instructions do not automatically release locked assets
2. **Status handling varies**:
- **Scheduled execution failures**: Instruction marked as "Failed" status
- **Manual execution failures**: Transaction reverts, instruction status unchanged
3. **Recovery options**: Use manual execution to retry once underlying issues are resolved
4. **Asset release**: Locks only release when instruction succeeds or is explicitly rejected
Common failure causes include compliance violations, issuer has frozen the asset or other transfer restrictions.
## Examples
### Peer to Peer Transfer
Alice wants to transfer 10 ACME tokens to Bob.
Alice creates an instruction at a venue (either one she owns or the default venue), with a single leg transferring 10 ACME tokens from her portfolio to Bob's portfolio.
#### Instruction Structure
```mermaid
flowchart LR
A("🏢 Venue ID: 42") --> B("📋 Instruction ID: 1337 Type: SettleOnAffirmation Status: Pending 📝 Memo: Gift from Alice")
B --> C("🔗 Leg 0: Fungible Transfer ━━━━━━━━━━━━━━━ 👤 From: Alice (0x01...abc) 📁 Portfolio: Default 💰 Asset: ACME (0x02...def) 📊 Amount: 10 tokens 👤 To: Bob (0x03...ghi) 📁 Portfolio: Default")
```
When Alice creates the instruction she affirms it and her 10 ACME tokens are immediately locked. The instruction now requires affirmation from Bob's portfolio.
Bob affirms the instruction, which triggers scheduling of the instruction for automatic execution since all required affirmations are now in place.
The settlement completes with Bob receiving 10 ACME tokens and Alice's tokens being unlocked from the instruction.
```mermaid
flowchart LR
A("Alice creates and affirms an instruction") --> B("10 ACME tokens locked from Alice")
B --> C("Bob affirms with his portfolio")
C --> D("Instruction executes automatically")
D --> E("Bob receives 10 ACME tokens")
```
### Exchange Mediated Transfer
ExchangeCo operates a trading venue and matches Alice (seller of ACME) with Bob (buyer of ACME) for an agreed exchange: 100 ACME for 10 USDC.
ExchangeCo creates an instruction at their venue with two legs:
- Leg 1: Transfer 100 ACME from Alice's portfolio to Bob's portfolio
- Leg 2: Transfer 10 USDC from Bob's portfolio to Alice's portfolio
#### Instruction Structure
```mermaid
flowchart LR
A("🏢 Venue ID: 15 ExchangeCo") --> B("📋 Instruction ID: 2468 Type: SettleManual Status: Pending 📝 Memo: ACME/USDC trade")
B --> atomic
subgraph atomic ["⚛️ Atomic Settlement"]
direction LR
C("🔗 Leg 0: Fungible Transfer ━━━━━━━━━━━━━━━ 👤 From: Alice (0x01...abc) 📁 Portfolio: Default 💰 Asset: ACME (0x02...def) 📊 Amount: 100 tokens 👤 To: Bob (0x03...ghi) 📁 Portfolio: Default")
D("🔗 Leg 1: Fungible Transfer ━━━━━━━━━━━━━━━ 👤 From: Bob (0x03...ghi) 📁 Portfolio: Default 💰 Asset: USDC (0x04...jkl) 📊 Amount: 10 tokens 👤 To: Alice (0x01...abc) 📁 Portfolio: Default")
end
style atomic stroke-dasharray: 5 5
```
When the instruction is created, assets are not locked until the senders affirm. Alice's 100 ACME tokens are locked when she affirms, and Bob's 10 USDC tokens are locked when he affirms.
Both Alice and Bob must affirm the instruction using their respective portfolios. Once both affirmations are received, ExchangeCo (the venue operator) manually executes the instruction.
The settlement completes with Alice receiving 10 USDC and Bob receiving 100 ACME.
```mermaid
flowchart LR
A("ExchangeCo creates 2-leg instruction") --> B("No assets locked initially")
B --> C("Alice affirms: 100 ACME locked")
B --> D("Bob affirms: 10 USDC locked")
C --> E("Both affirmed: Ready for execution")
D --> E
E --> F("ExchangeCo executes instruction manually")
F --> G("Alice gets 10 USDC Bob gets 100 ACME")
```
### Settlement with Off-Chain Receipt
ExchangeCo operates a hybrid settlement model where some transfers happen off-chain (like fiat payments) while others happen on-chain.
Alice wants to buy 100 ACME tokens from Bob for $1000 USD (paid off-chain via bank transfer).
ExchangeCo creates an instruction with two legs:
- Leg 1: Transfer 100 ACME from Bob's portfolio to Alice's portfolio (on-chain)
- Leg 2: Off-chain leg representing Alice's $1000 payment to Bob
#### Instruction Structure
```mermaid
flowchart LR
A("🏢 Venue ID: 15 ExchangeCo") --> B("📋 Instruction ID: 3579 Type: SettleOnAffirmation Status: Pending 📝 Memo: ACME for USD")
B --> atomic
subgraph atomic ["⚛️ Atomic Settlement"]
direction LR
C("🔗 Leg 0: Fungible Transfer ━━━━━━━━━━━━━━━ 👤 From: Bob (0x03...ghi) 📁 Portfolio: Default 💰 Asset: ACME (0x02...def) 📊 Amount: 100 tokens 👤 To: Alice (0x01...abc) 📁 Portfolio: Default")
D("🔗 Leg 1: Off-Chain Transfer ━━━━━━━━━━━━━━━ 👤 From: Alice (0x01...abc) 💵 Asset: USD (Off-chain) 📊 Amount: $1000 👤 To: Bob (0x03...ghi) 🧾 Requires: Signed Receipt")
end
style atomic stroke-dasharray: 5 5
```
When created, no assets are locked initially since ExchangeCo (not the asset owners) created the instruction.
Alice makes the $1000 bank transfer to Bob off-chain. Once Bob confirms receipt, an authorized signer of the ExchangeCo venue creates a signed receipt acknowledging the off-chain payment.
Bob affirms the instruction using `affirm_with_receipts`, providing:
- His portfolio for the on-chain leg
- The signed receipt for the off-chain leg
The instruction executes automatically, transferring 100 ACME to Alice while the off-chain receipt is recorded on-chain as proof of the fiat payment.
```mermaid
flowchart LR
A("ExchangeCo creates hybrid instruction No assets locked initially")
A --> prep
subgraph prep ["📋 Off Chain Actions"]
C("Alice sends $1000 off-chain to Bob")
D("Venue signer creates signed receipt")
C --> D
end
A --> affirm
subgraph affirm ["✅ Affirmation Phase"]
direction LR
E("Alice affirms: No assets to lock (receiver only)")
F("Bob affirms: 100 ACME locked")
end
G("Receipt submitted by anyone")
prep --> G
affirm ---> H("All requirements met: Instruction executes")
G --> H
H --> I("Alice gets 100 ACME Receipt recorded on-chain")
style prep stroke-dasharray: 3 3
style affirm stroke-dasharray: 3 3
```
### Netting Process
ClearCo operates a multilateral netting service and receives the following bilateral obligations from trading activity between BankCo_1, BankCo_2, BankCo_3, and BankCo_4. All obligations are in USDC:
**Raw bilateral obligations (pre-netting):**
- BankCo_1 owes BankCo_2: 1,200 USDC
- BankCo_2 owes BankCo_3: 700 USDC
- BankCo_3 owes BankCo_4: 900 USDC
- BankCo_4 owes BankCo_1: 800 USDC
- BankCo_2 owes BankCo_1: 300 USDC
- BankCo_3 owes BankCo_1: 400 USDC
- BankCo_4 owes BankCo_2: 500 USDC
ClearCo processes these and computes net positions per participant:
**Net positions:**
- **BankCo_1**: Net **receive** 300 USDC (receives 300 + 400 + 800 = 1,500, owes 1,200)
- **BankCo_2**: Net **receive** 700 USDC (receives 1,200 + 500 = 1,700, owes 700 + 300 = 1,000)
- **BankCo_3**: Net **send** 600 USDC (receives 700, owes 900 + 400 = 1,300)
- **BankCo_4**: Net **send** 400 USDC (receives 900, owes 800 + 500 = 1,300)
ClearCo constructs a minimal settlement instruction with net legs, reducing 7 gross payment flows down to 4 net settlement legs:
- BankCo_3 sends 600 USDC to ClearCo
- BankCo_4 sends 400 USDC to ClearCo
- ClearCo sends 300 USDC to BankCo_1
- ClearCo sends 700 USDC to BankCo_2
#### Instruction Structure
```mermaid
flowchart LR
A("🏢 Venue ID: 88 ClearCo") --> B("📋 Instruction ID: 4680 Type: SettleAfterLock Status: Pending 📝 Memo: Daily multilateral netting")
B --> atomic
subgraph atomic ["⚛️ Atomic Settlement"]
C("🔗 Leg 0: Net Inflow ━━━━━━━━━━━━━━━ 👤 From: BankCo_3 (0x33...ccc) 📁 Portfolio: Default 💰 Asset: USDC (0x04...jkl) 📊 Amount: 600 tokens 👤 To: ClearCo (0x88...zzz) 📁 Portfolio: Default")
D("🔗 Leg 1: Net Inflow ━━━━━━━━━━━━━━━ 👤 From: BankCo_4 (0x44...ddd) 📁 Portfolio: Default 💰 Asset: USDC (0x04...jkl) 📊 Amount: 400 tokens 👤 To: ClearCo (0x88...zzz) 📁 Portfolio: Default")
E("🔗 Leg 2: Net Outflow ━━━━━━━━━━━━━━━ 👤 From: ClearCo (0x88...zzz) 📁 Portfolio: Default 💰 Asset: USDC (0x04...jkl) 📊 Amount: 300 tokens 👤 To: BankCo_1 (0x11...aaa) 📁 Portfolio: Default")
F("🔗 Leg 3: Net Outflow ━━━━━━━━━━━━━━━ 👤 From: ClearCo (0x88...zzz) 📁 Portfolio: Default 💰 Asset: USDC (0x04...jkl) 📊 Amount: 700 tokens 👤 To: BankCo_2 (0x22...bbb) 📁 Portfolio: Default")
%% Grid layout: Row 1: C D, Row 2: E F
C ~~~ D
E ~~~ F
end
style atomic stroke-dasharray: 5 5
```
When created, no USDC amounts are locked initially since ClearCo (not the asset owners) created the instruction. Assets are only locked when each party affirms the instruction.
Each party affirms the instruction with their portfolio, which locks their sending amounts:
- BankCo_3's affirmation locks 600 USDC
- BankCo_4's affirmation locks 400 USDC
- ClearCo's affirmation locks 1,000 USDC for outgoing payments
Once all parties have affirmed, the instruction executes automatically. The netting settlement completes with significantly reduced payment volume and operational risk compared to settling all 7 bilateral obligations individually.
```mermaid
flowchart LR
A("ClearCo creates multilateral netting instruction") --> B("No assets locked initially")
B --> F1("BankCo_2 affirm: No assets locked (receiver only)")
B --> F2("BankCo_4 affirms: 400 USDC locked")
B --> C("BankCo_3 affirms: 600 USDC locked")
B --> D("BankCo_1 affirm: No assets locked (receiver only)")
B --> E("ClearCo affirms: 1,000 USDC locked")
subgraph affirm ["✅ Affirmation Phase"]
direction LR
F1
F2
E
C
D
end
F1 ~~~ F2
D ~~~ C
C --> G("All affirmed: Execute netting")
D --> G
E --> G
F1 --> G
F2 --> G
G --> H("7 bilateral obligations settled via 4 net legs")
style affirm stroke-dasharray: 3 3
```
### Security Token Offering
Security Token Offerings (STOs) are a special case that use Polymesh's settlement engine but behave differently from standard settlement instructions.
AcmeCo wishes to distribute 1000 ACME tokens to investors through a security token offering. When AcmeCo sets up the STO distribution, **their 1000 ACME tokens are immediately locked and committed to the distribution** - this acts as a blanket pre-affirmation for any investor who participates.
When Alice decides to invest and buy 100 ACME tokens for 10 USDC, the STO pallet creates a settlement instruction with two legs:
- Leg 1: Transfer payment (10 USDC) from Alice's portfolio to AcmeCo's portfolio
- Leg 2: Transfer ACME tokens (100 ACME) from AcmeCo's portfolio to Alice's portfolio
However, since AcmeCo's assets are already locked and pre-committed to the distribution, **the instruction can execute immediately in the same block** when Alice affirms with her payment.
#### STO Settlement Flow
```mermaid
flowchart LR
A("🏢 STO Distribution AcmeCo") --> B("📋 1000 ACME tokens locked & committed to distribution")
B --> C("📋 Alice's Investment Instruction ID: 5791 Type: SettleOnAffirmation Status: Pending")
C --> atomic
subgraph atomic ["⚛️ Atomic Settlement"]
direction LR
D("🔗 Leg 0: Payment ━━━━━━━━━━━━━━━ 👤 From: Alice (0x01...abc) 📁 Portfolio: Default 💰 Asset: USDC (0x04...jkl) 📊 Amount: 10 tokens 👤 To: AcmeCo (0x05...mno) 📁 Portfolio: Default")
E("🔗 Leg 1: Token Distribution ━━━━━━━━━━━━━━━ 👤 From: AcmeCo (0x05...mno) 📁 Portfolio: Default 💰 Asset: ACME (0x02...def) 📊 Amount: 100 tokens 👤 To: Alice (0x01...abc) 📁 Portfolio: Default ✅ Pre-affirmed")
end
style atomic stroke-dasharray: 5 5
```
**Key differences from standard settlement:**
1. **Pre-commitment**: When the STO distribution is created, AcmeCo's 1000 ACME tokens are locked and the issuer effectively provides blanket affirmation for any valid investment
2. **Immediate execution**: When Alice affirms with her 10 USDC payment, the instruction executes in the same block since AcmeCo's side is already committed
3. **Streamlined flow**: No separate affirmation step needed from the issuer for each individual investment
```mermaid
flowchart LR
A("AcmeCo sets up STO distribution") --> B("1000 ACME locked & pre-committed")
B --> C("Alice invests 10 USDC")
C --> D("Instruction created & Alice's payment locked")
D --> E("Immediate execution in same block")
E --> F("Alice gets 100 ACME AcmeCo gets 10 USDC")
```
:::info STO vs Standard Settlement
STOs use the same underlying settlement engine but with pre-committed issuer assets, enabling immediate execution when investors participate. This differs significantly from standard settlement instructions where all parties must explicitly affirm before execution.
For comprehensive details on STO mechanics, asset distribution, and investor participation, see the [Security Token Offering documentation](/sto/).
:::
---
## Settlement Sequence Diagrams
## Purpose of Sequence Diagrams
These sequence diagrams provide a detailed overview of the steps involved in the settlement process for Assets. They outline each step comprehensively, from account setup to instruction creation, affirmation, rejection, and execution.
It's important to note that these diagrams offer a low-level representation, including all steps of the listed process. This level of detail ensures clarity and precision in understanding the settlement process.
When using tools such as the Polymesh REST API or the Polymesh TypeScript SDK, some of the steps depicted in these diagrams are combined into a single user action. These tools can simplify the process for integrators, streamlining the integration of Assets.
:::note
These diagrams cover settlement flows for both traditional portfolio-based transfers and Account ID-based transfers. The mechanics are the same regardless of whether senders and receivers hold assets in portfolios or at Account IDs.
:::
## Asset Creation
In this sequence diagram, the asset issuer creates, configures, and issues an asset. In this flow, the issuer performs these actions directly, but it is also possible to delegate the configuration and issuance steps to an agent once the asset has been created.
```mermaid
sequenceDiagram
participant I as Issuer
participant P as Polymesh
Note over I: Issuers can directly issue, or specify an agent to issue on their behalf
opt
I ->> P: Register Asset Ticker
end
I ->> P: Create Asset
opt
I ->> P: Specify Asset Metadata
end
opt
I ->> P: Specify Asset Compliance Rules
end
I ->> P: Issue (mint) Tokens For This Asset
```
## Asset Agent Management
In this optional sequence diagram, the asset issuer grants an external agent permission to operate their asset on their behalf.
Agents can be granted granular permissions so that they can only operate specific features (e.g., documentation updates) as needed.
```mermaid
sequenceDiagram
participant I as Issuer
participant A as Agent
participant P as Polymesh
opt
I ->> P: Create a custom Permission Group for the asset
P -->> I: Returns Permission Group ID
end
I ->> P: Issue `BecomeAgent` Authorization
Note over I,P: Specifies Ticker, Agent DID, and Agent Permission Group
P -->> A: Pending Authorization Approval/Rejection
A ->> P: Accept `BecomeAgent` Authorization
Note over I, P: Once an Agent has been permissioned, they can act on behalf of the Issuer for the specified transactions with respect to this asset
```
## Asset Venue Creation
In this step, the asset issuer (or their agent) creates a new distribution venue to distribute their asset. Settlement instructions in Polymesh can optionally be associated with a particular settlement venue. Only the creator of that venue can create settlement instructions under that venue and optionally specify who is allowed to sign receipts for off-chain asset transfers.
In addition, the issuer of an asset can optionally manage which venues they allow their asset to trade within.
```mermaid
sequenceDiagram
participant I as Issuer / Agent
participant P as Polymesh
I ->> P: Create a Venue
Note over I,P: Venue Details, Type, and Receipt Signers provided
P -->> I: New Venue ID
Note over I,P: Venue ID is emitted in an event
opt
I ->> P: Restrict Asset Settlement Instructions to Specified Venues
end
opt
I ->> P: Update Venue Details for Issuer Venue
end
opt
I ->> P: Update Permitted Settlement Venues for the Asset
end
opt
I ->> P: Update Allowed Receipt Signers on Issuer Venue
end
opt
I ->> P: Update Venue Details for Issuer/Agent's Venue
end
```
## Portfolio Movement (Segregated Omnibus)
In this step, assets are moved between portfolios under the same entity. Portfolios can be used to segregate client funds under a single omnibus account or to organize assets into other logical partitions (e.g., asset type, investment purpose).
Since the asset is not being transferred between different entities, compliance rules and the usual affirmation settlement flow do not apply. Additionally, as movements are within the same entity, only a single transaction is required to perform the movement. A single portfolio movement transaction can contain multiple different assets.
The sequence diagram includes the steps needed to create portfolios, although this is a one-time operation.
```mermaid
sequenceDiagram
participant C as Custodian
participant P as Polymesh
par
C ->> P: Create New Portfolio Named "ClientA"
and
C ->> P: Create New Portfolio Named "ClientB"
end
C ->> P: Move Assets from Default Portfolio to ClientA
C ->> P: Move Assets from Default Portfolio to ClientB
C ->> P: Move Assets from ClientA to ClientB
```
## Asset Settlement / Distribution
In this step, a settlement instruction is settled between two counterparties, a sender and a receiver. In this example, both the sender and receiver configure a custodian who performs the affirmation step on their behalf, this approach is optional. The next example shows the sender and receiver affirming directly rather than via a custodian.
For clarity, roles have been clearly separated; however, some entities may perform multiple roles (e.g., the asset issuer may also be the sender in the case of primary distribution).
:::note
If the settlement instruction creator is also a sender or receiver in the instruction, then creating the instruction will also affirm the legs of the instruction that apply to them.
:::
```mermaid
sequenceDiagram
participant I as Venue Owner (e.g. Issuer / Exchange)
participant S as Asset Sender
participant R as Asset Receiver
participant SC as Sender Custodian
participant RC as Receiver Custodian
participant M as Mediator
participant P as Polymesh
par
S ->> P: Issue Custody Authorization for Sender Custodian
Note over P: Pending Custody Authorization Approval/Rejection from Sender Custodian
SC ->> P: Approve Custody Authorization
and
R ->> P: Issue Custody Authorization for Receiver Custodian
Note over P: Pending Custody Authorization Approval/Rejection from Receiver Custodian
RC ->> P: Approve Custody Authorization
end
I ->> P: Create Settlement Instruction between Sender and Receiver
par
SC ->> P: Affirm Settlement Instruction as Sender
Note over P: Sender Assets Locked
and
RC ->> P: Affirm Settlement Instruction as Receiver
Note over P: Receiver Assets Locked
and
opt
M ->> P: Affirm Settlement Instruction as Mediator
end
end
I ->> P: Execute Settlement Instruction (can alternatively be executed by any party to the instruction)
Note over P: Sender and Receiver Balances Updated and Locks Removed
```
## Asset Settlement / Distribution with Off-Chain Leg
In this step, we show the sequence when a settlement instruction involves both an on-chain leg (to move an asset issued directly on Polymesh) as well as an off-chain leg representing an asset or payment moving on a chain or payment system outside of Polymesh.
In this case, we have synchronized settlement between the off-chain and on-chain legs (rather than atomic settlement) with signed data provided via an "Off-Chain Signer API" to verify that the off-chain leg was completed before the settlement instruction is finalized on Polymesh.
:::note
The role of the "Off-Chain Signer API" entity in the below sequence diagram is:
- monitor Polymesh for settlement instructions related to specified venues which involve off-chain legs
- observe whether or not the off-chain leg has been completed on the external payment / chain
- once the above steps have been completed successfully, sign a receipt (referencing the Polymesh settlement instruction and off-chain leg) using a private key that has been permissioned as an allowed signer on the venue associated with the settlement instruction
This signed data can then be used on Polymesh to affirm the off-chain leg, meaning that the Polymesh settlement instruction cannot be finalized until the "Off-Chain Signer API" has confirmed the payment (off-chain) leg.
:::
```mermaid
sequenceDiagram
participant I as Venue Owner
participant S as Asset Sender
participant R as Asset Receiver
participant M as Mediator
participant O as Off-Chain Signer API
participant P as Polymesh
I ->> P: Create Settlement Instruction between Sender and Receiver
note over I,P: Settlement Instruction includes an on-chain asset (sender to receiver) and an off-chain asset (receiver to sender)
par
S ->> P: Affirm Settlement Instruction
P ->> P: Sender Assets Locked
and
note over R: Transfer the off-chain asset
R ->> O: Request Signed Receipt the Off-Chain Asset Has Been Transferred
O ->> R: Signed Receipt Provided
R ->> P: Affirm Settlement Instruction via Signed Receipt
and
opt
M ->> P: Affirm Settlement Instruction
end
end
I ->> P: Execute Settlement Instruction
Note over I,P: Fully affirmed instructions can optionally be executed by any party to the instruction
P ->> P: Sender and Receiver Balances Updated and Lock Removed
```
## Simplified Account ID Transfer (`transfer_asset`)
Polymesh provides a simplified API (`asset::transfer_asset`) for single-leg fungible asset transfers between Account IDs. This flow is much simpler than the multi-leg settlement instruction process because it auto-affirms the sender and automatically executes when the receiver affirms or if the receiver has pre-approved the asset.
For transfers that skip the affirmation cycle whenever nothing requires it — including third-party, allowance-drawn transfers — see [Direct Transfers (`transfer_funds`)](/settlement#direct-transfers-transfer_funds).
:::note
The `transfer_asset` function is designed for Account ID to Account ID transfers only. For transfers involving portfolios, first move the balance to an Account ID using `portfolio::move_portfolio_funds` or alternatively use the full settlement instruction flow shown above.
:::
```mermaid
sequenceDiagram
participant S as Asset Sender (Account ID)
participant R as Asset Receiver (Account ID)
participant P as Polymesh
S ->> P: Call transfer_asset with:
Note over S,P: Receiver Account ID, Asset ID, Amount, optional memo
par
P ->> P: Resolve sender and receiver DIDs from Account IDs
P ->> P: Create settlement instruction with single leg
and
P ->> P: Auto-affirm sender Lock sender assets
end
alt Receiver has pre-approved the asset
P ->> P: Auto-execute instruction Transfer assets immediately
P -->> S: Transfer complete
P -->> R: Assets received
else Receiver has NOT pre-approved
P -->> R: Pending affirmation required
Note over S,P: Instruction awaits receiver affirmation
alt Receiver affirms
R ->> P: Call receiver_affirm_asset_transfer
P ->> P: Affirm receiver and attempt execution
P ->> P: Transfer assets
P -->> S: Transfer complete
P -->> R: Assets received
else Receiver rejects
R ->> P: Call reject_asset_transfer
P ->> P: Release sender lock
P -->> S: Transfer rejected
end
end
```
---
## Checkpoint Management
## Overview
An asset checkpoint is a snapshot of all holder balances for a specific asset at a particular point in time. Checkpoints are essential for many corporate actions and compliance activities, providing a verifiable record of asset ownership that can be referenced for:
- **Corporate Actions**: Support key events like dividend distributions and voting
- **Regulatory Reporting**: Generate point-in-time records for compliance requirements
- **Ownership Analysis**: Track historical ownership patterns and changes
- **Income Distribution**: Process payments based on ownership at specific dates
- **Governance**: Enable voting weight calculations and participation tracking
## Core Concepts
### Balance Recording
Checkpoints track two types of information:
- **Total Supply**: The total amount of the asset in circulation
- **Individual Balances**: The amount held by each identity
### Lazy Updates
Checkpoints behave slightly differently depending on the checkpoint type:
- **Manual Checkpoints**:
- Total supply is recorded immediately when the checkpoint is created
- Individual balances are recorded lazily when they first change after the checkpoint
- **Scheduled Checkpoints**:
- The checkpoint only materializes when a balance-modifying transaction occurs at or after the scheduled time
- If no balance-modifying transaction occurs, the checkpoint never materializes on chain but remains as "pending" under the `checkpoint::ScheduledCheckpoints` chain storage entry
- When a checkpoint materializes:
- The total supply at that moment is recorded. If the transaction that triggered the checkpoint creation was issuing or redeeming an asset, the total supply is recorded before the change, to reflect the supply at the time of the checkpoint.
- Individual balances begin being recorded lazily as they change
- For unmaterialized checkpoints, current balances are considered correct for that checkpoint
This approach ensures accurate recording while optimizing storage costs.
## Creating Checkpoints
There are two methods for creating checkpoints:
### 1. Manual Checkpoints
Individual checkpoints can be created immediately using `checkpoint::create_checkpoint`. These are useful for:
- One-off corporate actions
- Ad-hoc reporting requirements
- Unscheduled distributions
- Emergency or compliance-driven balance snapshots
### 2. Scheduled Checkpoints
Scheduled checkpoints are created by providing an array of future timestamps using `checkpoint::create_schedule`. The schedule consists of:
- A sorted array of timestamps defining when checkpoints should be created
- Each timestamp specified in milliseconds since Unix epoch
- All timestamps must be in the future at creation time
A scheduled checkpoint materializes when the following criteria are met:
1. The scheduled timestamp has been reached
2. Any action that modifies a user's balance occurs (transfer, mint, burn)
3. No checkpoint has been created yet for that scheduled time
:::note
If multiple scheduled checkpoints have passed their timestamp when a balance-modifying action occurs, all of them will be materialized in order. This catch-up mechanism ensures no scheduled checkpoints are missed.
:::
:::note
Once created, schedule timestamps cannot be modified, the schedule can only be removed. A new schedule must be created if changes are needed.
`checkpoint::remove_schedule` removes an existing checkpoint schedule.
:::
## Accessing Checkpoint Data
To determine balances at a checkpoint, you need to query the available storage maps.
### Query Total Supply
The total supply at a checkpoint reflects the supply when the checkpoint was created. The chain storage `checkpoint::TotalSupply` when provided with the assetId and checkpoint ID returns the total supply at that checkpoint.
### Query Identity Balance
An identity's balance at a specific checkpoint is determined as follows:
- If the identity has had any balance changes after the checkpoint, the `checkpoint::BalanceUpdates` will include a checkpoint entry greater than or equal to the checkpoint of interest. Query `checkpoint::Balance` for the nearest checkpoint greater than or equal to the required checkpoint.
- If no balance changes occurred after the checkpoint there will be no checkpoint entry in the `checkpoint::BalanceUpdates` storage greater than or equal to the checkpoint of interest and you should use their current balance from `asset::BalanceOf`
:::note Important
Checkpoints are numbered sequentially starting from 1. This sequential numbering is important when querying checkpoint data, as you'll need to use these checkpoint IDs to access the relevant data.
The lazy update mechanism means balance history is only recorded when changes occur. When querying historical balances, you must account for whether balance updates exist for that checkpoint.
:::
### Query Checkpoint Creation Time
The `checkpoint::Timestamp` storage map provides the timestamp associated with a checkpoint, which can be used for:
- Verification of checkpoint timing
- Determining if a scheduled checkpoint was materialized
- Auditing and reporting purposes
## Best Practices
When implementing checkpoints:
1. **Schedule Planning**:
- Plan checkpoint timestamps carefully as they cannot be modified once scheduled
- Consider timezone implications when setting timestamps
- Account for any relevant business calendar requirements
2. **Performance Optimization**:
- Understand that storage costs are optimized through lazy updates
- Consider the frequency of checkpoints needed
- Remember that each scheduled checkpoint consumes chain resources
3. **Data Management**:
- Monitor checkpoint creation success
- Maintain schedule IDs for management
- Remove schedules if they are no longer required
:::note
Checkpoints are immutable once created. Ensure timing and asset selection are correct before creation.
:::
:::tip
When querying historical balances, remember that the lazy update mechanism means some checkpoint balances may be the current holdings rather than historical positions if no transactions occurred after the checkpoint.
:::
---
## Capital Distribution
## Overview
Capital distribution on Polymesh allows issuers to distribute payments to holders of a specific asset based on their holdings recorded at a particular [checkpoint](/corporate-actions/checkpoints). While commonly used for dividends, this functionality can support any type of payment or benefit distribution to asset holders. The process is managed on-chain, ensuring transparency and automation. Distributions are typically made using a separate asset, often a stablecoin representing fiat currency, but can be any Polymesh asset.
:::info Regulatory Compliance
Corporate actions like dividend distributions may trigger registration or filing requirements with applicable securities regulators or other governmental authorities. Always consult with legal and financial advisors before initiating a corporate action.
:::
## Core Concepts
- **Target Asset**: The asset whose holders will receive the distribution (identified by its `AssetId`).
- **Distribution Asset**: The asset used for payment (identified by its `AssetId`).
- **Checkpoint**: A snapshot of target asset holder balances at a specific time, used to determine entitlements.
- **Distributor**: The identity (usually the asset issuer or an [authorized agent](/asset-agents)) initiating and funding the distribution.
- **Payment Amount**: The total amount of the distribution asset allocated for the distribution.
- **Payment Per Share**: The amount of distribution asset paid for each unit of the target asset held.
- **Payment Date**: The date from which holders can claim their payments.
- **Expiry Date** (Optional): The date after which unclaimed distributions can be reclaimed by the distributor. If not set, the distribution never expires.
## Use Cases
- **Dividend Payments**: Distribute quarterly or annual profits to shareholders.
- **Interest Payments**: Distribute interest to bondholders or other debt security holders.
- **Royalty Distributions**: Share revenue with rights holders based on ownership percentages.
- **Incentive Payments**: Reward long-term holders or participants in governance.
- **Income Distribution**: Payments from funds or income-generating assets.
- **Liquidation Distributions**: Distributing remaining assets during company wind-down.
- **Rebates or Rewards**: Return capital based on usage, loyalty, or other metrics associated with asset holding.
## Distribution Lifecycle
The capital distribution process involves several key steps managed through specific transactions. **Crucially, before a distribution can be created, a corresponding [Corporate Action](/corporate-actions#2-initiate-corporate-action) must first be initiated** using `corporateAction::initiate_corporate_action`. This step defines the record date (via a checkpoint), target holders, tax settings, and generates the `CAId` (composed of the target `AssetId` and a local ID) required for the distribution.
### 1. Create Distribution
Once the underlying Corporate Action exists, the distributor attaches the distribution details using the `capitalDistribution::distribute` transaction.
**Parameters**:
- `ca_id`: Corporate Action Identifier **obtained from initiating the Corporate Action**. It contains:
- `asset_id`: The `AssetId` of the target asset.
- `local_id`: The sequential, chain-assigned local ID of the Corporate Action for this asset (auto-incremented when the CA was initiated).
- `portfolio`: The portfolio from which the distribution asset will be sourced.
- `currency`: The `AssetId` of the asset being distributed (the dividend currency).
- `per_share`: The amount of `currency` paid per unit of the target asset.
- `amount`: The total amount of `currency` to be distributed. This amount becomes locked in the specified `portfolio` for the purposes of the distribution.
- `payment_at`: The timestamp (specified in milliseconds since Unix epoch) when the payment period begins. Holders can claim from this point.
- `expires_at` (Optional): The timestamp when the distribution expires. If `None`, it never expires.
**Effects**:
- Creates the distribution record on-chain, linking it to the initiated Corporate Action.
- Locks the specified `amount` of `currency` in the distributor's `portfolio` for the purposes of the distribution.
- Defines the payment terms and timeline.
:::tip Combined Initiation and Distribution
For convenience, the `corporateAction` pallet provides the `initiateCorporateActionAndDistribute` extrinsic. This allows the Corporate Action Agent to initiate the CA and create the distribution in a single transaction, streamlining the process. This is generally preferred over using separate calls.
:::
:::warning Fund Depletion
Distributions operate on a "first come, first served" basis regarding the locked `amount`. If the total calculated entitlement based on `per_share` and the checkpoint balances exceeds the locked `amount`, funds might be depleted before all holders claim or receive their full share. Ensure the `amount` is sufficient to cover all entitlements.
:::
### 2. Claim Payments
Asset holders who were included in the target identities of the Corporate Action and who held the asset at the record date can claim their entitled payments once the `payment_at` date is reached by calling `capitalDistribution::claim`.
**Parameters**:
- `ca_id`: The Corporate Action Identifier for the distribution.
**Effects**:
- Calculates the holder's entitlement based on their balance at the specified checkpoint and the `per_share` rate.
- Considers any applicable tax withholdings configured for the corporate action.
- Transfers the net payment amount (post-tax) from the distributor's locked portfolio to the claimant's **default portfolio**.
- Updates the distribution record to reflect the claimed amount.
- Marks the holder as paid to prevent double claims (a holder can only claim once per distribution).
**Constraints**:
- Claims can only be made after the `payment_at` date has been reached.
- Claims cannot be made after the `expires_at` date (if specified) has passed.
- Each holder can only claim once per distribution.
- Only identities that were included in the Corporate Action's targeting (either directly or by not being excluded) are eligible to claim.
### 3. Push Benefit
The distributor (or an authorized agent) can proactively send the payment to specific holders, rather than waiting for them to claim, using `capitalDistribution::push_benefit`.
**Parameters**:
- `ca_id`: The Corporate Action Identifier.
- `holder`: The identity of the asset holder to receive the payment.
**Effects**:
- Similar to `claim`, but initiated by the distributor for a specific holder with the distributor paying the associated transaction fees.
- Calculates entitlement, applies taxes, and transfers the net amount to the target's **default portfolio**.
- Marks the specified holder as paid, preventing them from claiming the same distribution later.
**Constraints**:
- Can only be executed by identities with an Agent permission for the asset.
- Can only be pushed after the `payment_at` date has been reached.
- Cannot be pushed after the `expires_at` date (if specified) has passed.
- Cannot be pushed to holders who have already claimed or received a pushed benefit.
- Can only be pushed to identities included in the Corporate Action's target set (holders at the record date who weren't explicitly excluded).
### 4. Reclaim Funds
If an `expires_at` date was set and has passed, the distributor (the agent who created the distribution) can reclaim any remaining, unclaimed funds using `capitalDistribution::reclaim`.
**Parameters**:
- `ca_id`: The Corporate Action Identifier.
**Effects**:
- Checks if the distribution has expired.
- Unlocks any remaining `currency` in the distributor's portfolio that was locked for this distribution.
- Transfers the unlocked amount back to the distributor's available balance.
- Marks the distribution as reclaimed, preventing any further claims or pushes.
**Constraints**:
- Can only be executed after the `expires_at` timestamp has passed.
- Can only be executed once per distribution.
- Can only be executed by the same identity that created the distribution.
- Cannot be executed if no `expires_at` date was set.
:::warning Indefinite Lockup
If no `expires_at` is set, unclaimed funds remain locked indefinitely in the distributor's portfolio unless claimed or pushed. Distributions with no expiry should be used with caution, as they may lead to permanently locked funds.
:::
### 5. Remove Distribution
The distributor can remove a distribution entirely before its payment date, by calling `capitalDistribution::remove_distribution`.
**Parameters**:
- `ca_id`: The Corporate Action Identifier.
**Effects**:
- Removes the distribution record from the chain state.
- Can only be executed if the distribution hasn't started yet (current time < `payment_at`).
- Once the payment period begins (current time >= `payment_at`), a distribution can no longer be removed.
## Checkpoint Integration
Capital distributions rely heavily on [Checkpoints](/corporate-actions/checkpoints) to determine entitlements. The `record_date` specified during the `corporateAction::initiate_corporate_action` call determines which checkpoint is used. This checkpoint captures the balances of the target asset at a specific moment.
- Ensure the correct checkpoint (representing the desired record date) is associated with the Corporate Action ID used for the distribution.
- The checkpoint must exist _before_ the distribution is created.
## Withholding Tax and Fund Management
The `amount` specified when creating a distribution should include **both** the net amount to be transferred to asset holders **and** any withholding tax amounts.
### Tax Handling Process
1. When a distribution is created, the full `amount` (covering both net payments and withholding tax) is locked in the distributor's portfolio.
2. When a holder claims or is pushed a benefit:
- The full gross entitlement (including tax) is unlocked from the distribution's locked funds
- Only the net amount (after tax is withheld) is transferred to the holder
- The withheld tax amount remains in the distributor's portfolio, now unlocked and available for use
3. The distributor can then manage these unlocked tax funds separately (e.g., transferring to tax authorities as required).
:::caution Tax Management
The withholding tax mechanism only handles the calculation and withholding of taxes. The distributor is responsible for properly managing and remitting the withheld taxes to the appropriate tax authorities according to applicable regulations.
:::
:::warning Unclaimed Funds with No Expiry
If no `expires_at` is set and some holders never claim their benefits, both their share and the associated withholding tax would remain locked indefinitely in the distribution. This is why setting an expiry date is generally recommended.
:::
## Handling Asset Types (Divisible vs. Indivisible)
The nature of the **distribution asset** (`currency`) impacts calculations:
- **Divisible Assets**: Can represent fractional amounts (e.g., 1.50 units). Calculations are precise.
- **Indivisible Assets**: Can only exist in whole units (e.g., 1, 2, 3 units).
**Rounding Implications**:
- Initial benefit calculations (balance \* per_share) are done with full precision.
- Only the final **transfer amount** (post-tax gain) is rounded **down** to the nearest whole unit when distributing an **indivisible** asset.
- The full calculated benefit amount (including any fractional part) is unlocked from the distribution's funds.
- For indivisible assets, the difference between the full benefit amount and the rounded transfer amount will remain in the distributor's portfolio, but becomes available for use.
**Example (Indivisible Currency)**:
- Target Asset: STOCK (Divisible)
- Distribution Asset: BOND_CERT (Indivisible)
- Checkpoint Balance: Holder A has 10.5 STOCK.
- Per Share Rate: 0.75 BOND_CERT per STOCK.
- Tax Rate: 10%
1. **Gross Entitlement**: 10.5 STOCK \* 0.75 BOND_CERT/STOCK = 7.875 BOND_CERT.
2. **Tax Calculation**: 7.875 BOND_CERT \* 10% = 0.7875 BOND_CERT.
3. **Net Amount (Gain)**: 7.875 - 0.7875 = 7.0875 BOND_CERT.
4. **Amount Unlocked**: 7.875 BOND_CERT (full gross entitlement).
5. **Amount Transferred**: 7 BOND_CERT (gain rounded down to whole unit for indivisible asset).
6. **Amount Remaining in Distributor's Portfolio**: 0.875 BOND_CERT (unlocked and available).
:::info Reclaiming Indivisible Assets
While fractional amounts of indivisible assets cannot be transferred between portfolios (only whole units can be sent), fractions can remain unlocked in the distributor's portfolio after a claim or push of an indivisible asset. When the `reclaim` function is called after a distribution expires, the **entire** remaining amount is unlocked in the distributor's portfolio, including any fractional amounts of indivisible assets. This allows the distributor to recover all locked funds, even partial units of indivisible assets that couldn't be transferred during the distribution phase.
:::
## Distribution System Limitations
When working with capital distributions on Polymesh, it's important to be aware of the following limitations:
- **Fund Sufficiency**: Ensure the locked `amount` is adequate to cover all potential claims based on the checkpoint balances and `per_share` rate. Insufficient funds lead to a first-come, first-served payout until depletion.
- **Identity-Only Claims**: Only the asset owner (the identity/DID that holds the asset) can claim their benefit.
- **Default Portfolio Only**: Benefits are always delivered to the default portfolio of the claiming identity. Benefits cannot be directed to specific portfolios, even when the same identity has multiple portfolios containing the target asset. The calculation is based on the total asset balance across all of the identity's portfolios.
- **No Custodian Claims**: Custodians cannot claim benefits on behalf of their clients whose assets they custody. The asset owner must claim directly or the agent of the distribution must push them.
:::tip Using Push Benefits
For scenarios where asset owners cannot or should not need to claim themselves, the `push_benefit` function provides a way for the distributor to proactively deliver benefits to holders who might be unable to claim directly.
:::
---
## On-Chain Voting
## Overview
On-chain voting is a key feature of Polymesh's [Corporate Actions](/corporate-actions) framework, enabling issuers to conduct secure and transparent governance processes directly on the blockchain. This functionality supports activities such as electing board members, approving resolutions, or other corporate decisions.
The process leverages Polymesh's blockchain to:
- Define proposals and voting rules (the Ballot).
- Determine voter eligibility and voting power based on asset holdings at a specific time ([Checkpoint](/corporate-actions/checkpoints)).
- Record votes securely on-chain.
- Automatically tally first-preference votes.
- Provide a permanent, auditable record of the voting process and its outcome.
The chain logic automatically handles eligibility checks based on the CA's Checkpoint, enforces the voting period timing, and records the first-preference tally once voting is complete.
This on-chain mechanism ensures transparency, security, and efficiency for corporate governance processes involving asset holders.
## Core Concepts
### Corporate Action (CA)
The foundational on-chain event that initiates the voting process. A CA, typically of kind `IssuerNotice`, must be created first. It defines the context, links to the asset, specifies the [Record Date (Checkpoint)](/corporate-actions/checkpoints) for eligibility, and generates the `CAId` needed to attach and reference a ballot.
### Ballot
The specific details of the vote attached to a Corporate Action. It includes:
- **Motions:** The distinct questions or proposals being voted upon. Each motion has a description and potentially links to further information. A single ballot can contain multiple independent motions.
- **Choices:** The options available for each motion (e.g., 'Aye', 'Nay', 'Abstain').
- **Voting Period:** The start and end date/time during which votes can be cast.
- **Ranked-Choice Voting (RCV):** An optional setting allowing voters to rank their preferences.
:::info Ranked-Choice Voting (RCV)
Ranked-Choice Voting is a voting method that allows voters to rank candidates by preference rather than selecting just one option. In Polymesh's implementation, voters can specify fallback choices that would receive their vote if their first choice is eliminated. This approach can help achieve broader consensus, especially in elections with multiple candidates, by ensuring votes aren't "wasted" on candidates with little support. While Polymesh records all first preferences and fallbacks on-chain, the final RCV calculation (including elimination rounds and preference transfers) must be performed off-chain according to the organization's governance rules.
:::
### Checkpoint
A snapshot of asset holder balances at a specific block, determined by the `record_date` in the associated Corporate Action. [Checkpoints](/corporate-actions/checkpoints) are crucial for determining who is eligible to vote and their respective voting power.
### Voting Power
The default approach to voting power is proportional to the amount of the target asset held by an identity at the time of the Checkpoint. One unit of the asset typically equals one vote. Every voter's vote is recorded on chain, so if an alternate weighting of votes is required this can be processed off-chain.
### Vote Casting
The act of an eligible identity submitting their choices for each motion to the blockchain via a transaction (extrinsic).
### Tallying
The automated process of counting the _first-preference_ votes cast for each choice within each motion after the voting period closes. As all votes are recorded immutably on chain, if alternate voting power or more complex result processing such as Ranked-Choice Voting is required this can be tallied off-chain. The tallied results are then interpreted to determine the outcome (e.g., Passed, Failed) based on rules defined for the ballot.
## Ballot Lifecycle
The on-chain voting process involves several key steps managed through specific transactions. **Crucially, before a ballot can be created, a corresponding [Corporate Action](/corporate-actions#2-initiate-corporate-action) must first be initiated** using `corporateAction::initiate_corporate_action`. This step defines the record date (via a checkpoint), target holders, and generates the `CAId` (composed of the target `AssetId` and a local ID) required for referencing the ballot.
### 1. Create Ballot
Once the underlying Corporate Action exists, an agent of the asset attaches the ballot details using the `corporateBallot::attach_ballot` transaction. Alternatively the combined `corporateAction::initiate_corporate_action_and_ballot` transaction can be used to create a corporate action and attach a ballot in a single transaction.
**Parameters**:
- `ca_id`: Corporate Action Identifier **obtained from initiating the Corporate Action**. It contains:
- `asset_id`: The `AssetId` of the target asset.
- `local_id`: A local identifier for this specific corporate action.
- `range`: The `BallotTimeRange` defining:
- `start`: The timestamp when voting begins.
- `end`: The timestamp when voting ends.
- `meta`: The `BallotMeta` containing:
- `title`: The ballot's title.
- `motions`: An array of `Motion` objects, each with:
- `title`: The question or proposal being voted on.
- `info_link`: A link to more information about the motion.
- `choices`: An array of `ChoiceTitle` options voters can select.
- `rcv`: Boolean flag indicating whether Ranked-Choice Voting is enabled.
**Effects**:
- Creates the ballot record on-chain, linking it to the initiated Corporate Action which defines voter eligibility.
- Sets up the ballot structure including all motions and their choices.
- Establishes the voting period and RCV settings.
### 2. Modify Ballot Configuration (Optional)
Before the voting period begins (before `range.start` is reached), the ballot can be modified through several transactions:
**Transactions Available**:
- `corporateBallot::change_end`: Adjust the voting end date.
- Only modifies the `end` timestamp; `start` remains unchanged.
- `corporateBallot::change_meta`: Modify the ballot's structure.
- Updates the ballot title, motions, and/or choices.
- `corporateBallot::change_rcv`: Toggle Ranked-Choice Voting support.
- Enables or disables the RCV feature.
- `corporateBallot::remove_ballot`: Cancel and completely remove the ballot.
- Removes all ballot-related data from the chain.
**Constraints**:
- All configuration changes must be made before the `start` timestamp is reached.
- Any authorized agent for the asset can make these changes.
- Once voting begins, the ballot structure is locked and cannot be modified.
### 3. Cast Votes
During the active voting period (from `range.start` to `range.end`), eligible asset holders can cast their votes using the `corporateBallot::vote` transaction.
**Parameters**:
- `ca_id`: Corporate Action Identifier for the ballot.
- `votes`: An array of `BallotVote` objects representing votes for each choice across all motions, where each `BallotVote` contains:
- `power`: The amount of voting power assigned to this choice.
- `fallback`: Optional index pointing to another choice in the same motion as a fallback (only used when RCV is enabled).
**Voting Mechanics**:
- **Basic Structure**: A vote is submitted as a flat array of `BallotVote` objects, where each object represents a vote for one choice in one motion. The order must match the sequence of choices across all motions.
- **Multiple Motions**: The `votes` array is a flat structure containing votes for all choices across all motions in sequence. For example, if the ballot has 2 motions, with motion 1 having 3 choices and motion 2 having 2 choices, the `votes` array would contain 5 entries (3 for motion 1 + 2 for motion 2).
- **Vote Distribution Options**:
1. **Split Voting**: Divide your voting power among multiple choices in a single motion.
2. **Full Voting**: Assign all your voting power to a single choice.
3. **Partial Voting**: Use only a portion of your available voting power; the remainder counts as an abstention.
4. **Independent Motions**: Your full voting power can be reused for each separate motion in the ballot.
- **Example Distribution**: With 100 tokens of voting power:
- Motion 1: Choice A: 60 power, Choice B: 40 power, Choice C: 0 power
- Motion 2: Choice A: 100 power, Choice B: 0 power
- **Ranked-Choice Voting**: If RCV is enabled, each `BallotVote` can include a `fallback` field pointing to another choice index within the same motion:
- **Local to the Current Motion**:
- The fallback index is local to the motion it belongs to, meaning it only references choices within the same motion.
- It does not reference the overall votes array or choices in other motions.
- For example:
- Motion A: ["Yes", "No"]
- Motion B: ["Candidate 1", "Candidate 2", "Candidate 3"]
- A fallback index of 0 in a vote for Motion A refers to "Yes".
- A fallback index of 0 in a vote for Motion B refers to "Candidate 1".
- A vote cannot fallback to itself.
:::note Vote Modification
Voters can change their votes at any time during the active voting period by submitting a new `vote` transaction with different choices. Each new vote completely replaces any previous vote by the same identity. This flexibility allows voters to adjust their decisions right up until the voting end date (`range.end`).
:::
**Effects**:
- Records the voter's choices and assigned voting power.
- Updates the running tally of first preference votes for each choice.
**Constraints**:
- Can only be executed during the voting period (after `start` and before `end`).
- Voter must be included in the Corporate Action's target set.
- The total voting power used for each motion cannot exceed the voter's balance at the checkpoint.
- The number of vote entries must match the total number of choices across all motions.
- If RCV is disabled, all `fallback` fields must be `None`.
### 4. Tally Results
After the voting period ends (after `range.end`), the blockchain automatically finalizes the tally of first-preference votes for each choice in each motion.
**First-Preference Tallying**:
- The blockchain automatically maintains a running tally of votes for each choice as votes are cast.
- After the voting period ends, these tallies represent the final first-preference vote counts.
- The raw results are available on-chain via the `Results` storage map.
- All individual `votes`, including fallbacks if applicable, are also stored immutably on-chain should other tallying methods be required to determine the motion result.
**RCV Calculations**:
- If RCV was enabled, the on-chain system only stores raw votes and first-preference tallies.
- The complete RCV calculation (elimination rounds and vote transfers) must be performed off-chain by applications.
- All data needed for these calculations (including fallback preferences) is stored on-chain.
### 5. Interpret Results
After votes have been tallied, the interpretation of results is entirely at the discretion of the asset issuer according to their governance framework.
**Issuer Determination**:
- Polymesh records all votes immutably but does not enforce any specific interpretation rules.
- The asset issuer (or their authorized agents) determines what constitutes a successful outcome based on their own governance requirements.
- These requirements may be defined in the organization's bylaws, regulatory frameworks, or other governance documents that exist outside the blockchain.
**Common Interpretation Approaches** (implemented off-chain):
- **Simple Majority**: A choice wins if it receives more votes than any other choice.
- **Qualified Majority**: A choice passes only if it meets a predefined threshold (e.g., more than 66% of all votes cast or of eligible voting power).
- **Minimum Participation**: Some governance frameworks require a minimum level of participation (quorum) for a vote to be valid.
- **Ranked-Choice Processing**: For ballots with RCV enabled, the votes and their fallbacks can be processed according to various elimination and transfer algorithms.
**Result Implementation**:
- The blockchain provides a transparent, auditable record of all votes cast, but does not itself determine or enforce outcomes.
- Based on the interpreted results, the asset issuer may take further on-chain actions (e.g., executing a distribution based on a passed proposal) or off-chain actions (e.g., appointing a board member).
---
## Corporate Actions
## Overview
Corporate Actions (CAs) are events initiated by an asset issuer that affect the asset or its holders. Polymesh provides a framework for managing various CAs on-chain, automating processes like [capital distributions](/corporate-actions/distributions) and [shareholder voting](/corporate-actions/on-chain-voting).
The Polymesh Corporate Actions functionality is primarily handled by three pallets:
1. **`corporateAction`**: The base layer for initiating CAs, managing record dates, linking documents, and defining core parameters like taxes and target identities.
2. **`capitalDistribution`**: Manages the distribution of benefits (payments, dividends, interest, etc.) linked to a CA. See [Capital Distribution](/corporate-actions/distributions).
3. **`corporateBallot`**: Handles on-chain voting linked to a CA. See [On-Chain Voting](/corporate-actions/on-chain-voting).
:::info Regulatory Compliance
Corporate actions may trigger registration or filing requirements with applicable securities regulators or other governmental authorities. Always consult with legal and financial advisors before initiating a corporate action.
:::
## Core Concepts
- **Corporate Action (CA)**: An on-chain record representing an event related to a specific [asset](/core/assets). Each CA has a unique ID (`CAId`) composed of the asset's internal `AssetId` (UUID) and a sequential ID specific to that asset. The sequential ID is automatically assigned when a CA is created, starting at 0 and incrementing for each new CA associated with the same asset. When referencing a CA in API calls, both the asset ID and this local sequential ID are required to uniquely identify the specific corporate action.
- **Corporate Action Agent (CAA)**: An identity authorized to manage CAs for an asset. By default, this is the asset issuer, but it can be delegated to another identity as an [Agent](/asset-agents) of the asset.
- **Record Date**: The specific point in time, defined by a [Checkpoint](/corporate-actions/checkpoints), used to determine holder entitlements (e.g., who receives dividends or can vote).
- **CA Kind**: The type of corporate action, determining its purpose and associated functionality (e.g., `IssuerNotice`, `Reorganization`, `PredictableBenefit`, `UnpredictableBenefit`, `Other`).
- **Declaration Date**: The date the CA was officially declared by the issuer (off-chain). Recorded for informational purposes.
- **Taxes**: Optional withholding tax percentages (default and DID-specific) applied to distributions.
- **Targets**: The set of identities eligible to participate in the CA. By default, all asset holders are targeted, but this can be customized (e.g., include/exclude specific identities).
- **Documents**: Off-chain documents (e.g., prospectuses, notices) can be linked to a CA for reference.
## Corporate Action Lifecycle
### 1. (Optional) Set Asset-Level Defaults
To simplify repeated CA creation, the CAA can set default configurations at the asset level:
- **Default Withholding Tax**: Use `corporateAction::set_default_withholding_tax` to set a standard tax rate for distributions related to this asset.
- **DID-Specific Withholding Tax**: Use `corporateAction::set_did_withholding_tax` to set custom tax rates for specific identities.
- **Default Targets**: Use `corporateAction::set_default_targets` to define a standard set of included/excluded identities for CAs of this asset.
:::info Immutability of CA Parameters after Creation
When a CA is created, the asset-level defaults (target identities and tax withholding) are **copied** into the CA structure. Subsequent changes to asset-level defaults will only affect future CAs, not existing ones.
- You can modify asset-level defaults at any time using the functions above
- Changes to defaults don't affect existing CAs or their active distributions
- Once a CA is created, most of its parameters (including targets and tax settings) cannot be modified
- Only the record date can be modified (using `change_record_date`) and only before a distribution starts
This design ensures that ongoing distributions maintain consistent parameters throughout their lifecycle, regardless of changes to asset-level defaults.
:::
### 2. Initiate Corporate Action
This is the **essential first step** for any on-chain distribution or voting process. The CAA creates the CA record using `corporateAction::initiate_corporate_action`.
**Parameters**:
- `asset_id`: The internal `AssetId` (UUID) of the target asset.
- `kind`: The type of Corporate Action:
- `PredictableBenefit`/`UnpredictableBenefit`: For distributions like dividends
- `IssuerNotice`: For ballots/voting
- `Reorganization`: For reorganization of tokens
- `Other`: For generic uncategorised CAs
- `decl_date`: The off-chain declaration date (timestamp).
- `details`: Free-form text description (bytes) (max length defined by `maxDetailsLength`).
- `targets` (Optional): Specify target identities if different from the asset default or all holders.
- `default_withholding_tax` (Optional): Override the asset's default tax rate for this CA.
- `withholding_tax` (Optional): Provide DID-specific tax rates for this CA, overriding asset defaults.
- `record_date` (Optional): Specifies the checkpoint to use as the record date. Can be:
- An existing Checkpoint ID.
- A future timestamp for a scheduled Checkpoint.
- An existing Checkpoint Schedule ID.
:::note
While the `record_date` is an optional parameter when initiating a corporate action it is mandatory one is set before attaching a distribution or ballot to the CA.
:::
**Effects**:
- Creates the CA record on-chain with a unique `CAId`.
- Establishes the core parameters (kind, record date, taxes, targets) for subsequent actions like distributions or voting.
### 3. Attach Specific Functionality (Distribution/Voting)
Once the CA is initiated, specific functionality can be attached based on its `kind`:
- **For `PredictableBenefit`, `UnpredictableBenefit`**: Attach a distribution using `capitalDistribution::distribute` (or use the combined `corporateAction::initiate_corporate_action_and_distribute`). See [Capital Distribution](/corporate-actions/distributions) for details.
- **For `IssuerNotice`**: Attach a ballot using `corporateBallot::attach_ballot` (or use the combined `corporateAction::initiate_corporate_action_and_ballot`). See [On-Chain Voting](/corporate-actions/on-chain-voting) for details.
:::tip Combined Initiation and Distribution
For convenience, the `corporateAction` pallet provides the `initiate_corporate_action_and_distribute` and `initiate_corporate_action_and_ballot` extrinsics. These allows the Corporate Action Agent to initiate the CA (providing target `AssetId`, etc.) and create the associated distribution or ballot in a single transaction.
:::
### 4. Manage the Corporate Action (Optional)
After initiation, the CAA can manage certain aspects:
- **Change Record Date**: Use `corporateAction::change_record_date` to modify the record date _before_ the associated distribution/ballot starts, provided the CA `kind` allows it.
- **Link Documents**: Use `corporateAction::link_ca_doc` to link supporting documents to the corporate action.
- **Remove Corporate Action**: Use `corporateAction::remove_ca` to remove a CA, typically after it's completed or if it needs to be cancelled before activation. This will also remove any associated distributions or ballots if they haven't yet started.
## Further Reading
---
## Custody Management
## Overview
Polymesh is designed to support several common custody models, including the ability to assign custodianship of a portfolio to another [identity](/identity). This separates beneficial ownership (which always stays with the beneficiary's identity for corporate actions) from custodial control, where another entity manages the [assets](/core/assets) on behalf of the beneficiary.
When control of a portfolio is assigned, the custodian can:
- **Affirm, reject, and revoke affirmation** for [settlement](/settlement) instructions involving the portfolio.
- **Move assets between portfolios under the same DID**: The custodian can transfer assets between [portfolios](/portfolios) belonging to the same identity, as long as they have custody of the source portfolio.
- **Pre-approve or remove pre-approval for asset receipt** for the portfolio.
- **Leave the custody relationship** (return custody to the owner).
**Restrictions:**
- The default portfolio cannot have a custodian. A custodian must be assigned to a user portfolio.
- Account ID based holdings (`AssetHolder::Account`) cannot be placed under custody — see [Asset Holdings](/asset-holdings) for the alternative to portfolio-based holding.
- The custodian cannot move assets to portfolios under a different DID; only between portfolios of the same owner.
- The custodian cannot delete the portfolio.
- The custodian cannot rename the portfolio.
Any assets in a portfolio which has been assigned a custodian are managed exclusively by that custodian, and include any new assets which are transferred into the portfolio.
A portfolio can only be assigned to a single custodian at a time.
## Custody Models
Polymesh supports a range of custody models to meet diverse requirements, allowing users and institutions to select the approach that best fits their regulatory, operational, and business needs.
**Use cases include:**
- **Self-custody:** The asset owner manages their own assets and portfolios directly, retaining both beneficial and operational control.
- **Omnibus custody:** A custodian pools assets for multiple clients in a single portfolio, with client balances tracked off-chain.
- **Segregated custody (omnibus segregation):** A custodian manages assets for multiple clients, but each client’s assets are held in separate on-chain portfolios under the custodian’s identity, providing on-chain segregation.
- **Delegated control (portfolio custody):** The owner retains beneficial ownership, but delegates management and [settlement](/settlement) responsibilities for a specific portfolio to a custodian, who cannot move assets outside the owner’s identity.
- **Separate-identity (full segregation):** The custodian creates a separate on-chain identity for each client, providing the highest level of on-chain segregation and regulatory clarity.
These models can be combined or tailored to meet specific regulatory or operational requirements.
### Self-Custody
In the self-custody model, the asset owner retains full control and beneficial ownership of their assets and portfolios. All actions, including transfers, settlements, and corporate actions, are performed directly by the owner or their authorized keys.
:::note
Some regulated assets or jurisdictions may require the use of an approved custodian, and self-custody may not be permitted for all asset types. Always check the compliance requirements for each asset.
:::
### Custody with Full Control and Ownership
In the full control and ownership model, assets are transferred from the user's identity to the custodian's identity. On Polymesh, this means the custodian becomes both the owner of record and beneficial owner of the securities on-chain.
The custodian has full rights over these assets within the protocol, including the ability to transfer, manage, or dispose of them as they see fit. For all [corporate actions](/corporate-actions), the custodian is recognized as the beneficial owner.
To hold and manage these assets, the custodian must possess the necessary [compliance claims](/compliance) and satisfy all regulatory requirements. The custodian is also responsible for ensuring that any underlying compliance obligations for their clients are met according to the issuer's rules.
#### Omnibus
Custodian creates one omnibus portfolio and holds all clients assets in a single portfolio within the corresponding identity.
```mermaid
flowchart TD
CustodianID["Custodian Identity"]
OmnibusPortfolio["Omnibus Portfolio (all client assets pooled)"]
Client1["Client 1"]
Client2["Client 2"]
Client3["Client 3"]
AssetA["ASSET A"]
AssetB["ASSET B"]
AssetC["ASSET C"]
CustodianID -->|Owns| OmnibusPortfolio
Client1 -.->|"Deposits Assets"| OmnibusPortfolio
Client2 -.->|"Deposits Assets"| OmnibusPortfolio
Client3 -.->|"Deposits Assets"| OmnibusPortfolio
OmnibusPortfolio --> AssetA
OmnibusPortfolio --> AssetB
OmnibusPortfolio --> AssetC
Note1["Individual client balances tracked off-chain"]
OmnibusPortfolio --- Note1
```
While this model is very simple and easy to manage, it lacks many useful features available in the more sophisticated approaches described below. As all assets are held in a single portfolio, the custodian must maintain an off-chain ledger of client balances and transactions. Since asset movements do not use the Polymesh [settlement engine](/settlement), on-chain compliance rules are not enforced. This means that the custodian must ensure that all asset movements are compliant with the rules of the issuer and any other regulatory requirements.
#### Omnibus Segregation
Custodian has a single identity and segregates client assets by portfolio (aka sub-accounts).
```mermaid
flowchart TD
CustodianID["Custodian Identity"]
PrimaryKey["Primary Key (Full Access)"]
SecKey1["Secondary Key (Portfolio 1 Assets)"]
SecKey2["Secondary Key (Portfolio 2/3 Assets)"]
Client1["Client 1"]
Client2["Client 2"]
Client3["Client 3"]
Portfolio1["Portfolio 1 Client 1 Assets"]
Portfolio2["Portfolio 2 Client 2 Assets"]
Portfolio3["Portfolio 3 Client 3 Assets"]
AssetA1["ASSET A"]
AssetA2["ASSET A"]
AssetA3["ASSET A"]
AssetB1["ASSET B"]
AssetB2["ASSET B"]
AssetB3["ASSET C"]
PrimaryKey --> CustodianID
SecKey1 --> CustodianID
SecKey2 --> CustodianID
CustodianID ---> Portfolio1
CustodianID ---> Portfolio2
CustodianID ---> Portfolio3
Client1 -.->|"Deposits Assets"| Portfolio1
Client2 -.->|"Deposits Assets"| Portfolio2
Client3 -.->|"Deposits Assets"| Portfolio3
Portfolio1 --> AssetA1
Portfolio1 --> AssetB1
Portfolio2 --> AssetA2
Portfolio2 --> AssetB2
Portfolio3 --> AssetA3
Portfolio3 --> AssetB3
```
**Key features of Omnibus Segregation:**
- Each client's assets are held in a separate on-chain portfolio under the custodian's identity, providing clear on-chain segregation and transparency.
- The custodian can permission secondary keys at the portfolio level, allowing flexible and secure access control for each client's assets.
- The primary key of the custodian can access all client portfolios, while secondary keys can be restricted to specific portfolios or assets as needed.
- Asset movements between portfolios are always recorded on-chain and can be audited, but as these movements remain under a single on-chain identity, Polymesh protocol compliance rules are not automatically enforced for these internal transfers. The custodian is responsible for ensuring compliance and maintaining accurate records.
- The custodian identity is recorded as the beneficial owner for all assets in these portfolios, including for corporate actions.
- The custodian is responsible for maintaining compliance for all assets held in these portfolios, including any regulatory or issuer requirements.
This model is often used by custodians, asset managers, or broker-dealers who want to provide clients with segregated accounts while retaining operational efficiency and compliance oversight. It can also be extended by creating separate identities for large clients or business units, further enhancing segregation and control.
#### Separate Identities (Full Segregation)
In this model, the custodian provisions a separate on-chain identity for each client. Each client identity has its own DID, key set, portfolios, assets, and claims, and operates independently at the protocol level.
Unlike secondary-key delegation or custody within a single identity, there is no protocol-level parent-child identity relationship in this approach. Segregation is achieved by operating distinct identities rather than by nesting identities.
This approach is often preferred when custodians need strict client-level segregation, independent compliance treatment per identity, and clear regulatory boundaries. The trade-off is higher operational complexity in [key management](/identity/advanced/secondary-keys), [identity onboarding](/identity/onboarding), and [POLYX](/polyx) fee funding across many identities.
```mermaid
flowchart TD
CustodianOps["Custodian Operations Team"]
ClientID1["Client 1 Identity"]
ClientID2["Client 2 Identity"]
ClientID3["Client 3 Identity"]
Key1["Key Set for Client 1 Identity"]
Key2["Key Set for Client 2 Identity"]
Key3["Key Set for Client 3 Identity"]
Portfolio1["Client 1 Portfolios"]
Portfolio2["Client 2 Portfolios"]
Portfolio3["Client 3 Portfolios"]
AssetA["ASSET A"]
AssetB["ASSET B"]
AssetC["ASSET C"]
CustodianOps -.->|"Operates via designated keys"| Key1
CustodianOps -.->|"Operates via designated keys"| Key2
CustodianOps -.->|"Operates via designated keys"| Key3
Key1 --> ClientID1
Key2 --> ClientID2
Key3 --> ClientID3
ClientID1 --> Portfolio1
ClientID2 --> Portfolio2
ClientID3 --> Portfolio3
Portfolio1 --> AssetA
Portfolio2 --> AssetB
Portfolio3 --> AssetC
```
In this structure, each client identity is fully independent for asset management, compliance evaluation, and key control. It provides the strongest on-chain segregation, with governance and operations handled separately per identity.
---
### Custody with Beneficial Interest Separated from Asset Control
In this model, the user retains full beneficial ownership of their assets (for example, for [dividend payments](/corporate-actions/distributions) and [corporate actions](/corporate-actions)), but delegates control of a specific portfolio to a third-party custodian. The custodian can affirm or reject settlements, move assets between portfolios under the same DID, and manage day-to-day operations.
Polymesh provides dedicated custody functionality for on-chain [settlement](/settlement), enabling this separation of control and ownership. This approach is particularly useful for regulated entities or investors who must retain beneficial ownership for compliance or reporting purposes, but wish to delegate operational responsibilities.
The custodian can also invest (on behalf of their client) in any [STOs](/sto) using funds from the client’s portfolio that they are a custodian of.
:::note
This model follows a similar approach to ERC-2258 as described in the [Regulatory Approaches to the Tokenisation of Assets](https://www.oecd.org/content/dam/oecd/en/publications/reports/2021/03/regulatory-approaches-to-the-tokenisation-of-assets_da7ae482/aea35466-en.pdf).
:::
```mermaid
flowchart TD
Investor1["Investor 1 Identity"]
Custodian["Custodian Identity"]
Client2["Investor 2 Identity"]
PrimKey1["Investor 1 Keys"]
PrimKey2["Custodian Keys"]
PrimKey3["Investor 2 Keys"]
Portfolio1["Investor 1 Portfolio (Control Assigned)"]
Portfolio2["Investor 2 Portfolio (Control Assigned)"]
AssetA["ASSET A"]
AssetB["ASSET B"]
AssetC["ASSET C"]
AssetD["ASSET D"]
PrimKey1 --> Investor1
Investor1 -->|Owns| Portfolio1
Portfolio1 --> AssetA
Portfolio1 --> AssetB
PrimKey2 --> Custodian
Custodian -.->|"Can Transfer and Affirm"| Portfolio1
Custodian -.->|"Can Transfer and Affirm"| Portfolio2
PrimKey3 --> Client2
Client2 -->|Owns| Portfolio2
Portfolio2 --> AssetC
Portfolio2 --> AssetD
```
**Key features of Portfolio Custody:**
- **Separation of ownership and control**: The investor retains beneficial ownership while delegating operational control to a custodian. The investor's ownership is reflected in corporate actions, dividends, and regulatory reporting.
- **Security model**: The custodian has exclusive control over the assigned portfolio, and only the custodian can relinquish this control. This security design protects assets from unauthorized access, even if an investor's keys are compromised.
- **Compliance enforcement**: Unlike the omnibus models, all asset transfers between investors must go through the Polymesh settlement engine and are fully subject to on-chain compliance rules.
- **Portfolio management**: Each portfolio can only have one custodian at a time, but an investor can create multiple portfolios with different custodians for different purposes or asset types.
- **Operational restrictions**: Custodians can only move assets between portfolios under the same identity (DID), cannot delete portfolios, and cannot transfer assets to external identities without using the settlement engine.
- **Regulatory considerations**: This model is often preferred by regulated entities that must maintain beneficial ownership for compliance reasons but want to delegate operational responsibilities to qualified custodians.
- **Transparent governance**: For voting and other governance activities, the beneficial owner (investor) participates directly, maintaining a clear separation between custody services and ownership rights.
## Custody Portfolios
Creating custody portfolios on Polymesh allows asset owners to delegate the management of specific portfolios to third-party custodians, while maintaining beneficial ownership. This is particularly useful for regulated entities and institutions needing to separate asset control from ownership.
The custodian receives exclusive rights to manage the portfolio, including transferring assets.
### Custody Portfolio Lifecycle
There are two approaches to establishing portfolio custody in Polymesh:
1. **Creating a new portfolio with a custodian** - The custodian creates a new portfolio that will be owned by the client but immediately under the custodian's control
2. **Assigning custody of an existing portfolio** - The portfolio owner initiates an [authorization request](/authorizations) to transfer custody of an existing portfolio to a custodian
#### Approach 1: Creating New Portfolios with Custody
##### 1. Allowing an identity to create Portfolios
An identity can be permitted to create portfolios by calling `portfolio::allow_identity_to_create_portfolios` and entering the DID of the permitted identity.
This is particularly useful for institutional or custody models, where, for example, a custodian or service provider needs to create and manage portfolios on behalf of clients, but the beneficial ownership remains with the client's identity. This permits the custodian to call `createCustodyPortfolio` by passing the portfolio owner's identity.
This function can be revoked by calling `revoke_create_portfolios_permission` by entering the permissioned identity.
##### 2. Creating a Custody Portfolio
The permissioned custodian can then create a custodial portfolio by calling `portfolio::create_custody_portfolio` with the following parameters:
- `portfolio_owner_id`: The DID that will own the new portfolio.
- `portfolio_name`: The name of the custody portfolio.
The transaction results in a new numbered portfolio being created under the owner's identity. The portfolio is owned by the specified identity but is immediately under the custody of the caller (the custodian).
#### Approach 2: Assigning Custody of Existing Portfolios
For existing portfolios, you can transfer custody through the [authorization process](/authorizations):
##### 1. Requesting Portfolio Custody
The portfolio owner initiates a custody request through the authorization framework by calling `identity::add_authorization` with the authorization type `PortfolioCustody`, the target identity that will receive custody rights, the portfolio ID that will be placed under custody, and an optional expiry time.
When submitted, this creates a pending [authorization request](/authorizations) with a unique ID that the custodian can view and accept.
##### 2. Accepting Portfolio Custody
The target custodian can accept the custody request by calling `portfolio::accept_portfolio_custody` with the [authorization ID](/authorizations).
Upon acceptance, the custodian immediately gains custody rights over the specified portfolio. The portfolio owner retains beneficial ownership but cannot manage assets until custody is relinquished.
#### 3. Ending Portfolio Custody
If a custodian no longer wishes to manage the portfolio or cannot fulfill their duties, they can relinquish their custodian rights to a portfolio by calling `portfolio::quit_portfolio_custody`.
This function immediately ends the custodian's management rights over the portfolio, and the portfolio owner regains full control and management authority. Note that only the current custodian of a portfolio can invoke this function. The portfolio owner cannot unilaterally revoke custody.
## Querying Custody Information
Portfolio owners and custodians can access custody information through several query methods:
- `portfolio::portfolioCustodian` to query the custodian of a particular portfolio.
- `portfolio::allowedCustodians` shows the custodians allowed to create and take custody of portfolios on an owner's behalf.
- `portfolio::portfoliosInCustody`: to track all the [portfolios](/portfolios) in custody of a particular [identity](/identity).
---
## Portfolios
## Overview
In Polymesh, all [assets](/core/assets) (excluding the network native token [POLYX](/polyx)) are held at the [identity](/identity) level, most commonly in portfolios associated with an identity. This allows Polymesh to enforce [compliance](/compliance) in real time based on claims also held at the identity level.
Portfolios allow users to organize their assets underneath their identity, and to flexibly assign key permissions and custody. A particular asset can have different balances across portfolios within the same identity.
:::note
A portfolio is one of two ways an identity's assets can be held — the other being directly on a signing key's account. See [Asset Holdings](/asset-holdings) for an overview of both models and how to select between them.
:::
[Permissions](/identity/advanced/secondary-keys/#secondary-key-permissions) for keys can be defined at the portfolio level. Secondary keys are managed at the identity level by the [Primary key](/identity/advanced/primary-keys/), and can be granted access to specific portfolios, no portfolios or all portfolios controlled by an identity. This ensures that only authorized keys or can transact with assets in those portfolios.
:::note
For more details on portfolio control and custody models, see the [Custody](/portfolios/custody) page.
:::
## Key Features and Use Cases
- **Asset Management**: Portfolios hold balances of various assets (security tokens, stable coins, etc.) issued on the Polymesh chain.
- **Key Permissions**: Secondary keys can be permissioned to allow them interact with specific portfolios.
- **Portfolio Control**: [Control/Custody](/portfolios/custody) can be granted to other identities, allowing them to act on a portfolio's behalf, such as initiating transfers or moving between portfolios under that identity.
- **Portfolio Movements**: Assets can be transferred between portfolios under the same identity without restrictions, enabling flexible asset management.
- **Portfolio Transfers**: Assets can be transferred between portfolios of different identities, using the [Settlement pallet](/settlement) which enforces compliant transfers.
- **Multiple Portfolio Management**: Users can create, add, remove, or rename portfolios to organize their assets according to different needs or strategies. Each identity has a default portfolio, but additional numbered portfolios can be created for further segregation of assets.
- **Pre-approval for Asset Transfers**: Portfolios can be pre-approved to receive specific assets without manual affirmation, streamlining the transfer process for frequently traded assets.
## Identity, Portfolios, and Assets Diagram
This diagram illustrates how a Polymesh identity is structured, showing the relationships between identities, keys, portfolios, and the assets held within those portfolios. It highlights how permissions and custody can be managed at different levels.
```mermaid
graph TD
style Identities stroke-dasharray: 5 5
style Keys stroke-dasharray: 5 5
style Portfolios stroke-dasharray: 5 5
style Assets stroke-dasharray: 5 5
subgraph Identities
A[Identity: Alice]
C[Identity: Bob]
end
subgraph Keys
PK["Primary Key (Alice) Full permissions"]
PK2["Primary Key (Bob) Full permissions"]
SK1["Secondary Key 1 Permissions: Portfolios [Default, P2] Assets [All]"]
SK2["Secondary Key 2 Permissions: Portfolios [P2] Assets [AMZ]"]
end
subgraph Portfolios
P1["Alice's Default Portfolio"]
P2["Alice's Portfolio 2 (P2)"]
P3["Alice's Portfolio 3 (Control Assigned)"]
P4["Bob's Default Portfolio"]
end
subgraph Assets
A1[1000 APPL]
A2[20 FB]
A3[50 AMZ]
A4[10 FB]
A5[1000 MSFT]
A6[100 GOLD]
end
PK2 --> C
SK1 --> A
PK ---> A
SK2 --> A
A --> P1
A --> P2
A --> P3
C -.->|"Controlled by Bob"| P3
C -->P4
P1 --> A1
P1 --> A2
P2 --> A3
P2 --> A4
P3 --> A5
P4 --> A6
```
## Managing Portfolios
### The Default Portfolio
Every Polymesh identity has a default portfolio. When a specific numbered portfolio is not designated, this is where assets are received by default for asset transfers. Functionally this portfolio is the same as any other portfolio, but it is the only one that cannot be deleted or renamed.
### User Portfolios
In addition to the Default portfolio, users can create multiple numbered portfolios that can be uniquely named under a single identity. These are useful for segregating assets, managing permissions, or separating business activities.
Users can have as many user portfolios as they like. The ID of user created portfolios increments from 1. User created portfolios can be renamed at any time or deleted if it does not hold any assets.
### Permissions and Access
Most portfolio management functions must be called by the owner of the portfolio, using either the primary key or a secondary key with the appropriate permissions. If control/custody of a portfolio has been assigned to another identity, only the controlling identity can move assets out of that portfolio.
---
### Create a Portfolio
Portfolios can be created by calling `portfolio::create_portfolio` and providing a unique portfolio name as a parameter. If a portfolio is created by a secondary key, it must be given explicit permission for that portfolio to be able to manage the assets within it even if it created it.
### Rename a Portfolio
A portfolio can be renamed by calling `portfolio::rename_portfolio`. The default portfolio cannot be renamed.
### Delete a Portfolio
Portfolios can be deleted by calling `portfolio::delete_portfolio` and providing the portfolio ID as a parameter. Only empty portfolios can be deleted. The default portfolio cannot be deleted.
### Moving funds between Portfolios
Funds can be moved between portfolios by calling `portfolio::move_portfolio_funds`. Transfers between portfolios of the same identity are not subject to compliance restrictions. If the owner has assigned control/custody of the portfolio to another identity, only the controlling identity can move assets out of that portfolio.
Parameters:
- `from`: The DID and the ID of the sender's portfolio.
- `to`: The DID and the ID of the receiver's portfolio.
- `funds`: The type (`Fungible` or `NonFungible`), the asset ID and the amount or NFT IDs of the asset being moved between portfolios.
- `memo`: A short memo can be added to each token amount moved. This is publicly visible on the chain and can be used to identify the transaction.
:::info
To move assets between portfolios of different identities, the [Settlement](/settlement) process must be used.
:::
### Preapprove receiving an asset in a Portfolio
Users can pre-approve receiving an asset in a portfolio by calling `portfolio::pre_approve_portfolio`.
This can streamline the [settlement process](/settlement) for certain assets to be transferred into the portfolio without the need for explicit approval of a settlement instruction. This function is useful for assets such as stablecoins, frequently traded assets, or workflows where manual approval for each transfer is not required.
When a portfolio is pre-approved to receive an asset, and a settlement instruction is created involving that combination of asset and portfolio as the receiver, that leg is automatically marked as approved by the receiver without requiring explicit affirmation or approval for each instruction. This can allow an instruction to settle immediately without manual intervention by the receiver or the controller of the portfolio.
Parameters:
- `asset_id`: The asset that will be exempt from affirmation.
- `portfolio_id`: The ID of the portfolio that can receive the asset without affirmation.
### Remove pre-approval to a Portfolio
To revoke or delete a previously granted pre-approval for a portfolio to automatically receive a specific asset, call the `portfolio::remove_portfolio_pre_approval` function.
Parameters:
- `asset_id`: The asset that will be exempt from affirmation.
- `portfolio_id`: The portfolio that can receive the asset without affirmation.
### Querying Portfolio Information
- `portfolio::allowed_custodians` shows the custodians allowed to create and take custody of portfolios on an identity's behalf.
- `portfolio::portfolio_asset_balances` to query the asset balances of portfolios.
- `portfolio::portfolio_asset_count` shows how many assets with non-zero balance this portfolio contains.
- `portfolio::portfolio_custodian` to query the custodian of a particular portfolio.
- `portfolio::pre_approved_portfolios` to query all portfolios that don't need to affirm the receivement of a given asset.
---
## Confidential Assets Architecture
This page provides an architectural overview of Polymesh Confidential Assets (PCA), explaining the core design principles, system components, and how they work together to enable privacy-preserving transfers with regulatory compliance.
Polymesh Confidential Assets implement a sophisticated protocol for privacy-preserving asset transfers on Polymesh. The implementation uses account-based confidential state with zero-knowledge proofs, enabling fully anonymous transfers while supporting the multi-party settlement workflows required for regulated securities.
## Core Architectural Principles
The architecture is engineered around several non-negotiable principles that guide every design decision:
### 1. Full Anonymity with Constant Complexity
The architecture achieves full confidentiality while maintaining efficient on-chain operations:
- **Full anonymity**: Balances, transaction values, asset types, and participant identities remain private
- **Global anonymity set**: Any transaction's anonymity encompasses all accounts in the system
- **Constant-size proofs**: On-chain proof size is O(1) regardless of anonymity set size
This design resolves the limitation of many account-based privacy systems where transaction size grows with the anonymity parameter.
### 2. Non-Interactivity and Concurrency Handling
The architecture enables:
- **Non-interactive transfers**: Senders can initiate transfers to offline receivers
- **Concurrency resolution**: Circumvents the problem where incoming transactions invalidate pending outgoing proofs—a common issue in account-based anonymous systems
The pending counter mechanism allows multiple in-flight transactions without state conflicts.
### 3. Account-Based Model with Per-Asset State
The architecture uses an account-based model for confidential state:
- **Account-based state**: More intuitive "table of balances" representation compared to UTXO models
- **Per-asset commitments**: One account key can register many assets; each account-asset pair maintains its own commitment and pending counter, so proofs include only the state of the asset being transacted and do not grow with the account's other assets
This simplifies operations like Proof of Balance while maintaining privacy guarantees.
### 4. Regulatory Compliance by Design
Privacy and compliance are not in conflict but architected together:
- **Scoped visibility**: Asset issuers designate auditors/mediators with asset-specific decryption capabilities
- **Dual compliance model**: Retrospective audit access (auditors) and prospective control (mediators)
- **Receiver affirmation**: Explicit accept/reject authority prevents forced asset delivery, aligning with securities regulations
## System Architecture
The key architectural theme is **off-chain proof generation** with **on-chain verification and state transitions**. This separation enables privacy while maintaining public verifiability.
## System Components
```mermaid
%%{init: {"flowchart": {"curve": "linear", "nodeSpacing": 40, "rankSpacing": 55}}}%%
flowchart TB
subgraph "User Systems"
WALLET["Wallet / Client"]
PROVER["Proof Generation Service (off-chain)"]
WALLET --> PROVER
end
subgraph "Polymesh Chain"
VERIFY["Proof Verification (on-chain)"]
CURVE["Curve Trees (account/asset roots)"]
SETTLEMENT["Confidential Settlement state machine"]
LEGS["Encrypted Settlement Legs"]
CURVE --> VERIFY
LEGS --> SETTLEMENT
end
subgraph "Compliance"
AUDITOR["Auditors (retrospective access)"]
MEDIATOR["Mediators (prospective control)"]
end
PROVER -->|Proofs| VERIFY
WALLET -->|Txs| SETTLEMENT
%% Keep labels short to avoid clipping; attach as dotted annotations.
AUDITOR --> LEGS
MEDIATOR --> LEGS
MEDIATOR --> SETTLEMENT
AUDITOR -. "Decrypt legs" .-> LEGS
MEDIATOR -. "Decrypt legs" .-> LEGS
MEDIATOR -. "Affirm or reject" .-> SETTLEMENT
```
### Component Roles
**User Systems:**
- **Wallet/Client**: Manages user keys, account state, and transaction coordination
- **Proof Generation Service**: Computationally intensive ZK proof generation happens off-chain (e.g., WASM library, proof server or remote service)
**Polymesh Chain:**
- **Proof Verification**: Validators verify ZK proofs without learning private values—cryptographic guarantees ensure correctness
- **Curve Trees**: Accumulators that compactly represent valid account/asset states as roots; membership proofs demonstrate state validity without revealing which specific account
- **Settlement State Machine**: Manages the multi-party affirmation lifecycle for transfers
- **Encrypted Legs**: Store transfer details encrypted for designated parties (sender, receiver, auditors, mediators)
**Compliance Layer:**
- **Auditors**: Passive entities with decryption keys for asset-specific legs; enable retrospective compliance checks without gating transactions
- **Mediators**: Active participants who can decrypt and must affirm or reject settlements when required; provide prospective regulatory control
### Privacy and Verification Separation
This architecture achieves privacy through strategic separation:
1. **What chain validators verify**: Proof validity, nullifier uniqueness, accumulator membership, sufficient balance (cryptographically hidden)
2. **What chain validators never learn**: Specific account identities, transaction amounts, asset types, sender/receiver linkage
3. **What compliance sees**: Only asset-specific legs they're authorized for, through encrypted payloads
The curve-tree roots act as a "compressed representation" of all valid account states—validators can verify state transitions against these roots without examining individual account details.
## Confidential Account Structure
A single key pair can register many assets, but each asset gets its own on-chain confidential account state. Think of the key as the identity, and each asset registration as a separate state commitment. This keeps ZK proving scoped to the asset being transacted instead of growing with every asset the key controls.
### Private State (known only to account holder)
These components are never revealed on-chain:
- **Finalized balance** - The spendable amount for the asset
- **Pending counter** - Tracks in-flight transactions awaiting receiver claim; enables concurrent operations and Proof of Balance
- **Account key** - Used to generate zero-knowledge proofs for state transitions
- **Encryption key** - Used to encrypt/decrypt settlement leg payloads
- **Randomness** - Secret values that derive commitments and nullifiers
### Public State (published on-chain)
These components are visible to validators but don't reveal the private state:
- **State commitment** - Cryptographic commitment to the private state, stored in the curve tree accumulator
- **Nullifier** - Emitted when spending an old account state; prevents double-spending without revealing which account was updated
### Cryptographic Relationships
- **State commitment**: Each account state (balance, counter, asset id, etc.) is committed with a one-time PRF-derived randomness seeded from the holder's secret key and public asset context (identity, asset id, counter). After initialization, the randomness evolves deterministically from the prior value for every state update, and each resulting commitment is published to the curve-tree accumulator.
- **Nullifier**: Every state also yields a nullifier derived from the same deterministic chain of secret, per-asset randomness. That chain advances on each transition, so an old state's nullifier can't be reused (prevents double-spends).
- **Deterministic recovery**: Because commitments and nullifiers rely on PRF-derived, forward-evolving randomness (not ad-hoc randomness), a user who retains their secret key and asset context can recompute historical commitments and nullifiers to recover state if local data is lost.
:::info
Pseudorandom Function (PRF): A deterministic, secret-keyed function that produces values indistinguishable from random. It seeds the initial per-asset randomness; later randomness values are derived deterministically from the prior one, so no additional PRF calls are needed after initialization.
:::
### Per-Asset State Handling
The protocol isolates state per account-asset pair, not by forcing separate confidential accounts (separate keys) per asset:
- **Independent account-asset state**: Each registered asset gets its own commitment, balance, pending counter, and randomness. Proof witnesses stay scoped to the asset being transacted regardless of how many assets the account holds.
- **No cross-asset invalidations**: Counters advance independently per asset, so a pending proof for Asset B remains valid if the same account transacts Asset C.
- **Scoped oversight**: Auditor or mediator decryption keys are registered per asset, keeping oversight limited to the assets they govern without revealing holder assets they are not authorized to view.
State isolation lets a holder keep one confidential account key across multiple assets while proofs, counters, and oversight remain scoped per asset; this minimizes settlement scanning and key management overhead. Additional confidential accounts can optionally be used for operational separation.
## Settlement Overview
The settlement lifecycle, roles, and proofs are documented in [Settlement Workflow](/confidential-assets/settlement-workflow). Refer there for detailed mechanics; this page focuses on architectural components and cryptographic structure.
## Proof of Balance (PoB)
For PoB mechanics, trade-offs (auditor-assisted vs. generic), and counter-update transactions, see [Compliance & Regulation](/confidential-assets/compliance-and-regulation) and [Settlement Workflow](/confidential-assets/settlement-workflow).
## Compliance Hooks
Details on auditors, mediators, key privacy, and policy gating live in [Compliance & Regulation](/confidential-assets/compliance-and-regulation). This architecture page references those hooks but avoids duplicating process specifics.
## Architectural Significance
Polymesh Confidential Assets resolve a fundamental tension in blockchain-based asset systems: the need for transactional privacy versus regulatory compliance. The architecture achieves this through:
1. **Privacy by design**: Full anonymity with constant-size proofs integrated at the protocol level
2. **Compliance by design**: Scoped access and control points built into the settlement workflow
3. **Practical settlement**: Multi-party affirmation workflows matching real-world securities processes
4. **Scalable proof system**: Computational work scales with specific transactions, not global state
This architectural foundation enables tokenization of regulated assets while maintaining the privacy properties required for financial markets.
---
## Onboarding & Confidential Accounts
This page explains the protocol-level steps to create and use confidential accounts on Polymesh. It focuses on what you do off-chain (generate keys, initialize your prover) and what you do on-chain (register, update state), so readers can apply the concepts in any application or wallet.
## Prerequisites
Before you register a confidential account, you need:
- A Polymesh signing key and DID (to submit transactions)
- Access to a proof service (e.g., a WASM library or remote prover)
## Keys and Deterministic Randomness
Every confidential account relies on two key pairs and deterministically derived randomness. The keys establish identity and encrypt payloads; the randomness binds state transitions and enables recovery.
- **Account key:** Proves ownership inside zero-knowledge proofs and deterministically derives nullifiers for spent states.
- **Encryption key:** Encrypts settlement leg details so only authorized parties (sender, receiver, auditors, mediators) can read them.
- **PRF-derived randomness values:** Derived from the account secret key plus public asset context (identity, asset id, and registration counter) to seed the initial per-asset randomness. After seeding, randomness advances deterministically on each state transition. It blinds commitments and produces nullifiers in a way that can be reconstructed later if local state is lost.
:::info
Pseudorandom Function (PRF): a deterministic function keyed by the confidential account secret key that outputs values indistinguishable from random; used to derive the initial per-asset randomness. Subsequent randomness values are derived deterministically from the previous one.
:::
## Lifecycle: Off-chain → On-chain
At a high level, you first set up keys off-chain, then establish your confidential account on-chain, then register asset-specific state, and finally perform private state transitions as you transact.
### 1) Off-chain: Generate keys and initialize prover
Generate your account and encryption keys, then load them into your proof service.
- Generate the account public/secret key and the encryption public/secret key.
- Initialize the proof service with keys; you may also pre-derive the initial per-asset randomness if you already know which asset you'll register.
- Store keys securely (hardware wallet, secure enclave, or encrypted vault).
### 2) On-chain: Register confidential account (identity link)
Prove ownership of your account key and link it to your DID. This establishes your confidential identity on the chain.
- Produce a **registration proof** showing ownership of your account public key.
- Submit `registerAccounts` with the proof to link the account and encryption key to your DID.
### 3) On-chain: Register for a specific asset (asset-specific state)
Create the asset-specific on-chain state for your account. One key can register many assets; each asset has its own independent state commitment. The initial state has balance = 0 and in-flight transaction counter = 0, with randomness seeded via the PRF.
- Produce a **register-account-asset proof**.
- Submit `registerAccountAssets`; the chain records an initial zero-balance commitment for that asset under your account key.
:::note
One key can register multiple assets; each asset has an independent on-chain state commitment
:::
### 4) On-chain: State transitions (ongoing use)
When you transact, the old state is consumed and a new commitment is published. Validators verify the proofs without learning private values.
- Reveal a deterministic **nullifier** for the old state (prevents double-spend).
- Publish a new **state commitment** (updated balance and transaction counter) to the curve-tree accumulator.
- Most updates occur via the settlement flows; see [Settlement Workflow](/confidential-assets/settlement-workflow).
## Recovery and Ledger Scanning
If you lose local state, deterministic randomness lets you rebuild it from the ledger.
1. Recompute the initial per-asset randomness from your secret key and asset context (identity, asset id, and registration counter), then step it forward deterministically for each transition.
2. Scan the chain for involved transactions, matching nullifiers/commitments and rebuild your local state timeline.
3. Decrypt relevant leg payloads with your encryption secret key when visibility is needed.
## Best Practices
Practical tips to keep operations smooth and private:
- Keep your account and encryption secret keys separate from the Polymesh signing key used to submit transactions.
- Maintain secure backups of keys and the minimal context needed to recompute randomness (secret key and asset identifiers); avoid storing decrypted seeds on disk.
- Cache latest state commitments locally, updating after successful on-chain transactions; periodically verify against curve-tree roots.
For asset creation/mint and settlement details, see:
- [Asset Operations](/confidential-assets/asset-operations)
- [Settlement Workflow](/confidential-assets/settlement-workflow)
---
## Asset Operations
This page explains how to create a confidential asset, register confidential accounts for that asset, and mint balances.
What you can do here:
- Create a confidential asset with issuer-embedded compliance hooks (auditors and/or mediators)
- Register confidential accounts for that asset (asset-specific state commitments)
- Mint supply while keeping prior balances private
## Prerequisites
- A Polymesh DID and signing key to submit transactions
- A proof service (e.g., WASM library or remote prover) with your confidential account and encryption keys loaded
- Auditor and/or mediator encryption keys if the asset requires compliance participants
## 1) Create a confidential asset
Define the asset and its compliance configuration, then publish it on-chain.
- Capture asset details (name, symbol, decimals, optional metadata).
- Assign compliance participants: auditors for retrospective visibility and/or mediators for prospective control; provide their encryption public keys. (A minimum of one is required)
- Request the proof service to build the creation proof and submit `createAsset`.
- On success, the chain emits `AssetCreated` with the new asset ID and records the issuer → asset → compliance keys mapping for future lookups.
Outcomes:
- Asset exists on-chain with its compliance hooks baked in.
- Regulatory metadata is visible (who the issuer is and which auditor/mediator keys apply) without revealing any balances or future transactions.
## 2) Register your account for the asset
Confidential accounts are asset-specific: the same key can register many assets, but each asset gets its own state commitment. This keeps proving scoped to the asset you are using.
- Prerequisites: your confidential account is registered on-chain and confidential account keys are available to the proof service.
- Generate a **register-account-asset proof** that proves ownership of the account keys and initializes a zero balance for the chosen asset.
- Submit `registerAccountAssets` with the proof; cache the returned state commitment and leaf path locally.
Outcomes:
- The account is linked to the asset. The asset registration of the confidential account is publicly visible.
- A zero-balance commitment for that asset is inserted into the curve-tree accumulator; nothing about balances is revealed.
## 3) Mint supply (issuer only)
Minting increases supply for the issuer while keeping prior balances confidential.
- Prerequisites: issuer's account-asset state and leaf path available to the proof service.
- Produce a **minting proof**: spend the old state, reveal a nullifier for it, and create a new state commitment with the increased balance.
- Submit `mintAsset` with the proof; persist the updated state and leaf path for future actions.
Outcomes:
- Public total supply increases; the issuer's new confidential balance is committed on-chain, and the spent state is prevented from reuse via its nullifier.
- The old balance remains hidden; only the new commitment and proof validity are public.
## Operational tips
- Keep encrypted backups of account-asset state (commitments, leaf paths, counters) after each successful transaction.
- When using a remote proof service, encrypt seeds/state in transit and at rest.
---
## Settlement Workflow
# Confidential Assets: Settlement Workflow
This page describes the lifecycle and mechanics of confidential settlements (transfers) on Polymesh. For related setup and asset flows, see [Onboarding & Accounts](/confidential-assets/onboarding-accounts) and [Asset Operations](/confidential-assets/asset-operations).
## Overview
Confidential settlement is intentionally multi-phase to align with regulated-market requirements and architectural constraints:
- **Creation (publish)**: Encrypted legs and a settlement proof are recorded; no balances change.
- **Affirmations (consent)**: Sender/receiver (and mediator, if required) explicitly affirm or reject.
- **Claim (credit)**: Receiver claims to credit value into finalized balance; counters update accordingly.
Why this design?
- **Receiver affirmation**: Mirrors institutional securities workflows where receivers must accept obligations before credit.
- **Non-interactivity**: Parties act asynchronously; senders can initiate while receivers are offline.
- **Concurrency handling**: Pending counters prevent state conflicts and make Proof of Balance workable.
- **Compliance gating**: Mediators provide prospective control when asset policy requires it.
## Roles in a Settlement
Confidential settlements involve clearly defined roles:
- **Sender:** Affirms the leg that transfers value. When affirmed, their spendable balance is debited and the pending counter increases.
- **Receiver:** Explicitly affirms acceptance of value; later claims to credit the incoming amount into the finalized balance.
- **Mediator (optional):** Active compliance participant who must affirm or reject when required by asset policy; gates execution.
- **Auditor (optional):** Passive compliance participant who can decrypt asset-specific leg details for retrospective verification; does not gate execution.
This dual model (mediators vs. auditors) provides prospective control and retrospective visibility without sacrificing on-chain confidentiality.
## Settlement Lifecycle
At a high level, a confidential transfer/settlement has three phases:
1. **Creation**: publish the encrypted legs and a settlement proof.
2. **Affirmations**: sender/receiver (and optionally mediator) record their decision.
3. **Claim/finalization**: the receiver claims and credits the value.
Why split it this way?
- Receiver affirmation is a real-world requirement for institutional securities-style transfers.
- The protocol must support asynchronous parties (senders and receivers do not need to be online at the same time).
### Lifecycle (detailed)
```mermaid
flowchart TD
A[Create Settlement] --> B[Status: Pending]
B --> C{Affirmations}
C -->|Sender Affirms| D[Balance Deducted Counter +1]
C -->|Receiver Affirms| E[Counter +1]
C -->|Mediator Affirms| F[Mediator OK]
D & E & F --> G{All Required Affirmed?}
G -->|No| C
G -->|Yes| H[Settlement Executes]
H --> I[Receiver Claims Assets Counter -1]
H --> J[Sender Updates Counter Counter -1]
I & J --> K[Finalized]
C -->|Someone Rejects| L[Settlement Rejected]
L --> M{Sender Affirmed?}
M -->|No| K
M -->|Yes| N[Sender Reverts Balance Restored Counter -1]
N --> K
```
### Core flow diagram
```mermaid
sequenceDiagram
participant C as Settlement Creator
participant CH as Polymesh Chain
participant S as Sender
participant R as Receiver
participant M as Mediator
participant PS as Proof Service
Note over C,CH: Create settlement (publish only)
C->>CH: Query asset registry (auditor/mediator keys)
C->>CH: Query asset curve-tree root
C->>PS: Build legs + SettlementProof(root, registry)
PS-->>C: SettlementProof bytes
C->>CH: createSettlement(proof)
CH->>CH: Record pending settlement
CH-->>C: Emit SettlementCreated event with id
Note over S,CH: Sender discovery + affirmation
S->>CH: Fetch settlement metadata(id) + encrypted leg
S->>S: Try decrypt with encryption key
S->>CH: Query account curve-tree root + leaf path
S->>PS: SenderAffirmationProof(oldState, leafPath, leg)
PS-->>S: SenderAffirmationProof
S->>CH: senderAffirmation(proof)
CH->>CH: Debit finalized
CH->>CH: Counter +1
CH->>CH: Update accumulator
Note over M,CH: Mediator action (if required)
M->>CH: Fetch encrypted leg
M->>M: Try decrypt with encryption key
M->>PS: MediatorProof(accept|reject)
PS-->>M: MediatorAffirmationProof
M->>CH: mediatorAffirmation(proof)
Note over R,CH: Receiver discovery + affirmation
R->>CH: Fetch encrypted leg
R->>R: Try decrypt with encryption key
R->>CH: Query account curve-tree root + leaf path
R->>PS: ReceiverAffirmationProof(oldState, leafPath, leg)
PS-->>R: ReceiverAffirmationProof
R->>CH: receiverAffirmation(proof)
CH-->>CH: Execute when all required parties affirmed
Note over R,CH: Receiver claim (credit)
R->>CH: Query updated root + leaf path
R->>PS: ReceiverClaimProof(leafPath, leg)
PS-->>R: ReceiverClaimProof
R->>CH: receiverClaim(proof)
CH->>CH: Credit finalized
CH->>CH: Counter -1
CH->>CH: Update accumulator
Note over S,CH: Sender counter update (PoB helper)
S->>CH: Query updated root + leaf path
S->>PS: CounterUpdateProof(leafPath)
PS-->>S: CounterUpdateProof
S->>CH: senderUpdateCounter(proof)
CH->>CH: Counter -1
CH->>CH: Update accumulator
alt Mediator rejects
Note over S,CH: Sender revert (restore)
S->>CH: Query updated root + leaf path
S->>PS: RevertProof(leafPath, leg)
PS-->>S: RevertProof
S->>CH: senderRevert(proof)
CH->>CH: Restore balance
CH->>CH: Counter -1
CH->>CH: Update accumulator
end
```
## Creating a Settlement
A settlement is created by defining the transaction legs (sender, receiver, asset type, and value) and generating proofs that ensure both cryptographic validity and regulatory compliance. The creator (who may be a separate entity from the sender or receiver) publishes:
1. **Encrypted leg data** — Encrypted for multiple parties: the sender, receiver, and any designated auditors/mediators retrieved from the on-chain regulatory list. This enables selective visibility for compliance oversight without revealing details to the public.
2. **Zero-Knowledge Settlement Proof** — A non-interactive zero-knowledge proof (NIZK) demonstrating:
- **Ciphertext well-formedness:** All encrypted values (identities, amounts, asset types) are correctly constructed.
- **Regulatory consistency:** The asset type and its compliance rules match the on-chain regulatory registry.
- **Value integrity:** The transfer amount is positive and within system bounds.
The proof also uses accumulator membership to hide which specific asset type is being transferred, maintaining unlinkability while still verifying regulatory compliance.
**Chain Recording:** Upon validation, the chain assigns a unique settlement identifier and records it in a pending state. Importantly, no balances are deducted yet—the sender's funds remain spendable until they explicitly affirm the settlement. The settlement remains "in transit" awaiting affirmations from required parties (sender, receiver, and mediator if applicable), enabling asynchronous participation where parties need not be online simultaneously.
## Find and decrypt a settlement
All parties to a settlement (sender, receiver, mediator, auditor) must first discover the settlement and decrypt the relevant leg details to verify their involvement and inspect the transaction terms before taking action.
**Discovery and decryption process:**
- Fetch settlement by ID from chain (received from another party or discovered via on-chain scan), pull encrypted legs.
- Attempt decryption with your account or encryption keys (sender/receiver/mediator/auditor).
- If decryption succeeds, you are a party to this settlement; save the settlement ID or decrypted leg details locally for verification and proof generation.
- **Verify the details:** Inspect the sender, receiver, asset type, amount, and any regulatory conditions before deciding to affirm or reject.
### Verifiability and Privacy (Decrypting Leg Data)
Leg payloads are encrypted so only designated parties can read them (sender, receiver, and asset-specific mediator and auditor keys). A public registry maps assets to their mediator and auditor keys, enabling:
- **Scoped visibility:** Authorized parties can decrypt and inspect the leg details for assets they oversee.
- **Public confidentiality:** Observers cannot read private values; they only see commitments, nullifiers, and proof validity.
This design preserves privacy on the public ledger while supporting per-asset auditability and optional prospective control via mediation.
:::note
Account balances are not included in leg ciphertexts. Balances are represented as commitments and validated via zero-knowledge proofs, so even authorized decryption of a leg does not reveal a participant's overall balance.
:::
## Sender affirmation
After discovering and decrypting the settlement (see [Find and decrypt a settlement](#find-and-decrypt-a-settlement)), the sender verifies the leg details and submits an affirmation proof to commit their assets to the transfer.
**Effect on balance and counter:**
- The sender's **spendable balance is immediately reduced** by the transfer amount.
- The **pending counter increases by 1**, placing the debited amount in a pending state.
- This prevents double-spending: the same assets cannot be committed to multiple settlements until the original settlement is finalized or reverted.
**Process:**
- Preconditions: account registered on-chain; access to account keys; locally cached account-asset state.
- Build account leaf path (root + curve tree path) and prepare the decrypted leg.
- Proof service produces a **sender affirmation proof** that: (a) spends the old state, (b) debits amount, (c) emits a nullifier, (d) increments counter.
- Submit the `senderAffirmation` transaction to persist the updated state (new commitment/index).
In the underlying construction, the sender proves membership of their old committed state in the accumulator, reveals a nullifier for the old state, and posts a new commitment reflecting the update.
## Receiver affirmation
After discovering and decrypting the settlement (see [Find and decrypt a settlement](#find-and-decrypt-a-settlement)), the receiver verifies the leg details and decides whether to affirm (accept) or reject the incoming transfer.
**Process:**
- Preconditions: account registered on-chain; access to account keys; locally cached account-asset state, leaf path, decrypted leg.
- Proof service produces a **receiver affirmation proof** that increments the pending counter (no balance change).
- Submit the `receiverAffirmation` transaction to store the updated state.
The affirmation step records consent and increments the counter, enabling the receiver to claim the value after settlement execution.
## Mediator affirmation or rejection
After discovering and decrypting the settlement (see [Find and decrypt a settlement](#find-and-decrypt-a-settlement)), the mediator inspects the leg details and decides whether to affirm (permit) or reject the settlement based on regulatory policy.
- Uses only encryption keys (no account balance state).
- Proof service produces a **mediator affirmation proof** with `accept=true` (approve) or `accept=false` (reject).
- Submit `mediatorAffirmation`; rejection halts the settlement until the sender reverts.
Until mediation is complete, the settlement remains pending.
## Receiver claim
After the settlement has been executed (all required parties have affirmed), the receiver submits a **claim** proof that credits the incoming value into their finalized balance and reduces the pending counter.
- Inputs: updated leaf path, decrypted leg, account state.
- Proof service generates the **claim proof** to credit the incoming amount and decrement the counter.
- Submit `receiverClaim`; persist new state.
This separation is important: it allows regulated "accept/reject first, credit later" semantics.
## Sender counter update
- After execution, sender can generate a **counter update proof** to decrement the counter (no balance change) and make future PoB proofs cheaper.
- Submit `senderUpdateCounter`; persist new state.
## Sender revert
- If a mediator rejects, a sender who already affirmed can generate a **revert proof** to restore the debited amount and decrement the counter.
- Submit `senderRevert`; persist new state.
## Counter mechanics (PoB relevance)
- Affirmations add to the pending counter; claims/reverts/counter-updates subtract from it.
- Lower counters shrink the footprint of later Proof of Balance proofs by reducing pending legs that must be referenced.
### Counter System Summary (Impact by Role)
| Role | Action | Impact on Balance | Impact on Counter |
| ------------ | ------------------------------ | ----------------- | ----------------- |
| **Sender** | Affirm | −Amount | +1 |
| **Sender** | Revert (after affirming) | +Amount | −1 |
| **Sender** | Update Counter (after execute) | — | −1 |
| **Receiver** | Affirm | — | +1 |
| **Receiver** | Claim Assets (after execute) | +Amount | −1 |
| **Mediator** | Affirm or Reject | — | — |
Counters make PoB proofs accountable to pending obligations while keeping transaction history private.
## Reversion and counter update
These operations maintain flexibility and efficiency: reversion lets senders reclaim funds from rejected settlements (see [Sender revert](#sender-revert)), while counter updates reduce PoB proof size by batch-finalizing completed legs (see [Sender counter update](#sender-counter-update)).
## Querying On-Chain Settlement Information
Applications and authorized participants can inspect settlement state via chain queries. Typical queries include:
- **Settlement state:** Returns the current status (e.g., pending, executed, finalized) for a given settlement reference.
- **Encrypted legs:** Returns the encrypted leg payloads by index; only authorized keys can decrypt.
- **Memo:** Returns any optional memo attached to the settlement.
- **Pending affirmations:** Returns how many affirmations are still required before execution.
- **Pending finalizations:** Returns how many post-execution steps (claims or counter updates) remain before finalization.
These queries enable UIs and services to track lifecycle progression without revealing confidential values.
## Batchability and Multi-Party Transactions (MPT)
Polymesh Confidential Assets (PCA) support batching multiple proof operations into a single transaction for efficiency. Most operations require an accumulator membership proof (the heaviest part of ZKP computation), so combining related actions reduces transaction count and proving work while preserving confidentiality.
### Supported batching patterns
- **Multi-party transfers (one sender → many receivers):**
- A sender publishes multiple encrypted legs to different receivers in one account state transition.
- The sender's proof shows the total debit equals the sum of all leg values, requiring just one accumulator membership proof.
- **Multi-party affirmations (one receiver ← many senders):**
- A receiver scans for pending legs addressed to them and affirms several in one transaction.
- The proof demonstrates they can decrypt each selected leg and that the credited amount equals the sum of affirmed values.
- **Aggregatable reversions (sender reclaim):**
- A sender reclaims funds from multiple un-affirmed legs in a single submission.
- The proof shows the sender authored the referenced legs and updates their balance by the aggregate reversed amount in one state transition.
- **Efficient account maintenance (PoB helpers):**
- **Counter updates (CU):** Batch pointers to finalized legs to reduce the pending counter in one step.
- **Pending-balance updates (PBU):** Batch updates to reduce hidden pending balance records in one step.
- **Atomic instant settlements:**
- Parties can combine `ExecuteInstantSettlement` with `InstantSenderAffirmation` and `InstantReceiverAffirmation` into a single atomic batch.
- This creates and fully affirms a settlement in one transaction, assuming off-chain coordination of commitments.
- Unlike the normal multi-phase flow, instant settlements update balances for both parties without changing counters (sender balance decreases, receiver balance increases, both counters remain unchanged as the complete settlement executes in a single transaction).
- **Mixed operational batching:**
- A single submission can include different proof types, for example: a receiver claim for an older leg, a new sender affirmation, and a sender counter update—all verified together.
### Important properties
- **Independent leg tracking:** Each leg's per-party affirmation status (sender affirmed, receiver affirmed, mediator decision, etc.) is tracked independently on-chain. This granular tracking allows the system to record which parties have acted on each leg. However, if any leg in a settlement is rejected (e.g., by a mediator), the entire settlement is rejected. Whether remaining legs can be reversed or claimed depends on the settlement's overall status.
- **Per-party decryption responsibilities:** Batching does not compromise participant privacy. Each receiver must still independently discover and decrypt only the legs intended for them using their unique encryption key; unauthorized parties cannot see other participants' leg details even within a batch.
- **Amortized proving efficiency:** Batching multiple actions—such as instant settlements or multi-receiver transfers—into a single transaction reduces the cost per action. The most computationally expensive part is the accumulator membership proof; batching allows multiple actions to share a single membership proof and state transition, reducing total on-chain submissions and verification overhead compared to separate transactions.
## Operational considerations for batching
Batching improves efficiency but introduces practical nuances you should account for:
- **Concurrency safety:**
- Only the account owner can submit transactions that update their account state. This design prevents a scenario where another user's settlement execution invalidates your pending proof (e.g., your receiver affirmation proof becomes invalid because a third party's settlement changed the chain state you built your proof against).
- The chain stores a window of recent accumulator roots so a proof against a slightly older root remains valid during broadcast, improving reliability with relayers and enabling asynchronous multi-party affirmations.
- **Linkability trade-off in maintenance:**
- Batching multiple finalized references into one update creates ephemeral linkability between the referenced items (observers can tell those references were processed together in one account update, but amounts and identities remain hidden).
- If maximum privacy is required, perform updates sequentially; if cost and simplicity are the priority, batch them.
- **Fee proof priority (relayer flow):**
- The fee proof is verified first. If it fails, the transaction is rejected immediately, preventing unfunded, complex batches from consuming resources.
- **Integrity of batched legs:**
- Sender-side MPT proofs must still ensure value conservation (total debit equals sum of leg values) and correct construction of ciphertexts.
- Receiver-side affirmation proofs must still prove decryptability and correct crediting, regardless of batching.
---
## Compliance & Regulation
# Compliance & Regulation in Confidential Assets
Polymesh Confidential Assets are designed for regulated assets. Instead of treating compliance as an afterthought, they introduce **asset-specific access hooks** that can support retrospective audit and prospective control while keeping the public ledger confidential.
## Roles
| Feature | Auditors | Mediators |
| ------------------------ | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| **Role in Lifecycle** | Hidden, passive compliance entities. | Hidden, active participants in the transaction lifecycle. |
| **Function** | Can decrypt and inspect encrypted transaction details for a specific asset type (retrospective auditability). | Must explicitly affirm or reject transfers when required (prospective control). |
| **Impact on Settlement** | Do not delay settlement; enable post-facto checks. | Settlement remains pending until affirmed/rejected. |
Notes:
- **Asset-specific:** Auditors/mediators are designated per asset type.
- **Scoped visibility:** They decrypt encrypted payloads for the assets they are assigned to.
- **Key-privacy requirement:** Polymesh publishes an on-chain record mapping asset types to their auditor/mediator keys. If ciphertexts revealed which key was used, an observer could identify the asset type. The protocol therefore requires encryption schemes that hide both the message and which key was used (key-privacy), preventing asset-type leakage while keeping transaction amounts confidential.
## Receiver Affirmation
Receivers explicitly accept or reject incoming assets before the incoming value becomes spendable.
Why this matters:
- It matches how regulated transfers often work: the receiver may assume legal/tax obligations.
- It prevents “forced delivery” of assets.
- It implies **multi-step settlement**: you need at least a sender-side action and a receiver-side action before a transfer is fully finalized.
## Proof of Balance (PoB)
Proof of balance (PoB) lets a verifier ask: “What is your balance for asset X?” without requiring the prover to reveal their full transaction history.
PoB is needed because receiver affirmation creates _in-between_ states: balances may be split between a finalized balance and pending legs.
### Approach 1: Auditor-assisted
- Prover reveals (and proves) finalized balance and pending counter to the asset's auditor.
- Prover points to relevant legs; auditor decrypts those legs (for that asset) and checks totals.
### Approach 2: Generic PoB
Allows proving balance to any verifier without auditor decryption.
- If all finalized + pending legs are referenced, the verifier can link those legs together.
- Counter-update transactions reduce counters associated with finalized legs so PoB can reference fewer historical items and leak less linkage.
## Operational Controls
:::note Future Features
The following operational controls are planned for future releases and are not currently implemented in Polymesh Confidential Assets.
:::
### Account freezing
Prevent further updates (or gate them) for an account/asset context in exceptional cases by proving the targeted account state without revealing its private values.
### Forced transfer (clawback / key loss recovery)
Support policy-driven recovery flows:
- **Key loss recovery:** re-encrypt value under a newly generated key.
- **Clawback:** enable issuer-controlled recovery flows under policy.
---
## Anonymous Fee Management
# Anonymous Fee Management: Ensuring Unlinkability through the Relayer Flow
To protect user privacy while ensuring network transaction costs are paid, Polymesh Confidential Assets (PCA) employ a sophisticated transaction **Relayer Flow** centered around a **Pooled System Account**. This mechanism decouples the identity of the user from the public payment of fees, enabling fully anonymous on-chain fee settlements.
## Overview
Unlike off-chain relayer reimbursement (where a user pays a relayer privately and the relayer covers fees from their own account), the Relayer Flow enables users to pay network fees through the blockchain using their confidential POLYX balance, while maintaining full anonymity. All user fee contributions flow through a single system-wide pooled account, where individual contributions are indistinguishable.
## The Pooled Fee System
All relayer fee payments are managed through a single system-wide account. This pool acts as a **cryptographic mixing vault** where individual user contributions remain anonymous. Observers can see the total amount of POLYX entering and leaving the pool, but cannot identify which confidential account was credited with an encrypted balance or how those funds are later spent.
The on-chain address for the Pooled System Fee Account is:
- **Mainnet SS58 address:** `2EvQQsqdCSiF6Xm4Tr4TrWb4LovaozjtPsmt9QxRRZYp3mNq`
- **Testnet SS58 address:** `5EYCAe5ijAx3nHtydpTBBYZojNDQTVdW46PpbvFTKDPSVRQy`
- **Hex public key:** `0x6d6f646c706d2f64617274660000000000000000000000000000000000000000`
## Fee Account Registration
A user begins by registering a **confidential fee-paying account** specifically for the POLYX asset type. At registration, this account is initialized with a **balance of zero**.
Because it is a confidential account:
- Its identity is hidden within the global accumulator
- Its state is represented as a cryptographic commitment
- No public record links this account to the user's identity
## Top-up (Funding the Pool)
Funding a fee account is a **protocol-driven, atomic operation** and must **not** be performed via a standard POLYX transfer to the pool account.
The process works as follows:
1. The user generates an **account state transition** for their confidential POLYX account
2. This transition produces:
- A new **cryptographic commitment** representing the updated state
- A **secret witness** that the user retains (used for future fee proofs)
3. Simultaneously, the protocol executes a **linked public transfer** of POLYX into the Pooled System Fee Account
4. While the transfer is visible on-chain, it is **cryptographically bound to the hidden commitment**
:::warning
A manual transfer to the system account without this state transition results in **irrecoverable funds**. The protocol requires the atomic coupling of the confidential state transition and the public transfer.
:::
**Privacy guarantee:** During top-up, the creditor's public key, confidential account public key and amount are **visible on-chain**. However, observers cannot identify how those funds are later spent in subsequent fee payments (where the confidential account public key becomes hidden and POLYX is transferred from the Pooled System Fee Account)
## Anonymous Fee Proof Generation
When a user wants to execute a confidential asset transfer via a relayer service and needs to pay network fees, they generate a **Zero-Knowledge Fee Proof**.
This proof demonstrates:
- The user possesses sufficient **hidden POLYX balance** within the Pooled System Account
- The balance covers both the **network fee** and **relayer commission**
- Without revealing:
- Which specific top-up belongs to the user
- Their total remaining fee balance
- Any linkability to previous transactions
### Public fee values vs. confidential proof
- **Fee and commission are public values:** The network fee and relayer commission are not encrypted for the relayer. They are public inputs to the fee proof and are not part of the ZKP witness.
- **Sufficiency without revelation:** The fee proof shows the user's hidden fee balance is sufficient to cover those public amounts, without revealing the balance itself or which top-up it came from.
- **Accumulator membership:** The proof also shows the user's confidential fee account state is valid and unspent in the global accumulator.
- **DoS protection for relayers:** Relayers verify the fee proof first, so they do not spend their own POLYX to broadcast a junk transaction.
## Anonymous Submission & Relayer Broadcast
1. The user sends their ZKP to a relayer via an **anonymous channel** (e.g., VPN or Tor)
2. The relayer **verifies the fee proof first**, checking for:
- Valid cryptographic proof
- Sufficient fee amount (DoS protection)
3. Upon validation, the relayer:
- **Signs** the transaction with their own key
- **Broadcasts** it to the Polymesh network
- Uses their own POLYX to cover the initial execution cost
## Automatic Reimbursement from Pool
Once the chain validates the ZKP:
1. The protocol **deducts the fee and commission** from the user's hidden state in the Pooled System Account
2. The user's confidential POLYX commitment is updated to reflect the reduced balance
3. The relayer is **automatically reimbursed** the POLYX fee and commission from the pool
**On-chain visibility:**
- The public sees POLYX movement from the Pooled System Account to the relayer
- The specific user connection remains **cryptographically obscured**
- No observer can determine which confidential account funded the fee
## Key Differences from Off-Chain Relayer Services
| Aspect | Off-Chain Relayer | Anonymous Fee Proofs |
| ------------------------ | ------------------------------------- | -------------------------------------------- |
| **User pays relayer** | Via private channel (off-chain) | Via confidential on-chain proof |
| **Network fees paid by** | Relayer's public account | Pooled System Account |
| **Proof of payment** | Trust/contract outside chain | Cryptographic on-chain proof |
| **Privacy** | Relies on off-chain secrecy | Built into protocol mechanism |
| **Traceability** | Off-chain payment could leak identity | On-chain movement is mixed in pool |
| **Fee overhead** | Standard transaction fee | Additional on-chain processing for fee proof |
:::note Fee Overhead
Transactions submitted via a relayer that include an anonymous fee proof require a slightly higher network fee to cover the additional on-chain processing required to verify the fee payment proof and transfer POLYX from the pool in addition to the main confidential asset transaction.
:::
## Summary
By using a pooled system account and Zero-Knowledge Proofs, the Relayer Flow ensures that:
- Users can pay network fees anonymously through the blockchain
- The total amount of POLYX flowing through the pool is visible (for transparency)
- The specific connection between a user and their fee payment is **cryptographically obscured**
- It is effectively a **private tab maintained within a public vault**
This mechanism is essential for applications requiring full on-chain anonymity while still respecting the network's requirement that transaction fees be paid in POLYX.
---
## Comparisons
# How Confidential Assets Compare to Other Privacy Approaches
Polymesh Confidential Assets (PCA) are not “just ZK”. They target **full anonymity**, an **account-based model**, and a **regulated settlement (transfers) workflow** (receiver affirmation, optional mediation, and scoped audit access).
This page gives a conceptual comparison to other common privacy approaches.
## Property Matrix
The matrix below summarizes practical properties across representative systems. Symbols: ✓ = supported; ✗ = not supported; △ = possible with reasonable modifications. Transaction size is shown relative to anonymity parameter `k`.
| System | Full anonymity | Proof of balance | Concurrency | Receiver affirmation | Non-interactive | Multi-party tx | Tx size |
| :------------------ | :------------: | :--------------: | :---------: | :------------------: | :-------------: | :------------: | :------: |
| Zerocash | ✓ | ✗ | ✓ | ✗ | ✓ | ✗ | O(1) |
| Monero | ✗ | ✗ | ✓ | ✗ | ✓ | ✓ | O(k) |
| zkLedger | ✓\* | ✓ | ✓ | ✗ | ✓ | ✓ | O(k) |
| QuisQuis | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | O(k) |
| Zether | ✗ | △ | ✗ | ✗ | ✓ | ✗ | O(k) |
| Anonymous Zether | ✗ | △ | ✗ | ✗ | ✓ | ✓ | O(k) |
| Veksel | ✗ | ✗ | ✓ | ✗ | ✓ | ✗ | O(1) |
| Platypus | ✓ | △ | ✓ | ✓\* | ✗ | ✗ | O(1) |
| PEReDi | ✓ | △ | ✓ | ✓\* | ✗ | ✗ | O(1) |
| PriDe CT | ✗ | △ | ✗ | ✗ | ✓ | ✓ | O(k) |
| PARScoin | ✓ | ✗ | ✓ | ✗ | ✓ | ✗ | O(1) |
| Ocash | ✓ | ✗ | ✓ | ✗ | ✓ | ✓ | O(1) |
| PriFHEte | ✓ | △ | ✗ | ✗ | ✓ | ✗ | O(1) |
| **Polymesh (DART)** | **✓** | **✓** | **✓** | **✓** | **✓** | **✓** | **O(1)** |
Notes:
- ✓\* under “Full anonymity” indicates systems that target small user sets.
- ✓\* under “Receiver affirmation” reflects interactive designs where receiver affirmation is inherent.
- This summary is adapted from public research for clarity; naming and headings are adjusted for implementation focus.
## A quick mental model
- **UTXO privacy systems** (Zcash-style): spend and create notes; anonymity sets are often phrased in terms of coins/notes.
- **Account-based privacy systems** (Zether-style): balances live under accounts; transactions often form an anonymity ring of size `k`.
- **Polymesh Confidential Assets**: keep an account-style abstraction while targeting full anonymity and constant-size on-chain proofs, and add regulated-transfer mechanics.
## Compared to UTXO-based shielded payments (Zerocash/Zcash-style)
Similarities:
- Both rely on commitments + zero-knowledge proofs.
- Both use nullifier-like constructs to prevent double-use.
- Both can have constant-size transactions relative to anonymity set size.
Key differences:
- **Bookkeeping model:** UTXO systems distribute a user's balance across many notes; Confidential Assets use account-style state transitions.
- **Proof of balance:** Confidential Assets target PoB (with counters and optional counter-updates). UTXO designs typically don't provide PoB out of the box because balances are spread across many notes.
- **Receiver affirmation:** Confidential Assets include accept/reject semantics as a first-class feature.
- **Settlement legs:** Confidential Assets are designed around multi-leg settlement (transfers) workflows (more like capital-markets settlement than simple payments).
## Compared to ring-based privacy (Monero-style)
Ring-based systems typically provide sender ambiguity via ring signatures and often have transaction size that grows with the ring size `k`.
PCA target:
- larger anonymity sets (conceptually “all participants”),
- constant-size on-chain proofs relative to anonymity set size,
- explicit regulatory flow (affirmation, mediation, auditing).
## Compared to account-based rings (Zether / Anonymous Zether-style)
These designs often select a ring of size `k` (sender/receiver plus dummies) and prove correctness over that ring.
Typical trade-offs in ring-based account privacy:
- transaction size and verification work can scale with `k`,
- concurrency constraints (epoch-based updates, state rollovers) may appear,
- anonymity depends on ring selection.
The design goal is to avoid a small fixed `k` anonymity set and to avoid requiring validators to touch “every account” on each transaction.
## Compared to mixers and application-layer privacy
Mixers can provide unlinkability for specific assets but usually:
- don't integrate receiver-affirmation style settlement,
- don't provide per-asset compliance access controls,
- may not support multi-leg regulated settlement flows.
PCA bake these requirements into the protocol design.
## Compared to privacy rollups
Rollups can hide details inside an off-chain execution environment and publish proofs/data to L1.
PCA differ in that:
- settlement workflow and compliance hooks are designed at the protocol level,
- the model is described as an account-based anonymous payment/settlement system rather than a general-purpose private VM.
## Benefits and trade-offs (practical)
Benefits:
- Regulated-market semantics (receiver affirmation, mediation).
- Scoped compliance access (auditors/mediators per asset type).
- Full anonymity goal with constant-size on-chain proofs.
Trade-offs:
- Proof generation cost and client complexity (wallets typically need to track state and scan for relevant legs).
- PoB is non-trivial: counters and counter-updates exist to balance verifiability against privacy leakage.
- Key management becomes more complex (account keys, encryption keys, and signing keys).
---
## Glossary
# Confidential Assets Glossary
This glossary defines terms used across the Confidential Assets documentation.
## Foundational Cryptographic Primitives
### Public Key Encryption (PKE)
Encrypts settlement payloads (e.g., leg details) so only designated parties can read them.
**Key-privacy is a protocol requirement.** Standard encryption hides the message content but may leak which public key was used. Polymesh publishes an on-chain record mapping each asset type to its designated auditor/mediator keys. If a ciphertext revealed which key was used, an observer could reference this mapping to identify the asset type being transacted. The protocol mandates key-privacy-preserving encryption so that ciphertexts do not leak which auditor key was used, maintaining asset-type confidentiality.
### Commitment Schemes
Hide values while binding the prover to them.
In Confidential Assets, commitments are used to represent **account states** on-chain without revealing balances.
### Pseudorandom Functions (PRF)
Deterministically derives secret values from secret keys and context.
Used to generate randomness/nullifiers in a way that can be reproduced by the rightful holder.
## Protocol Concepts
### DART vs. Polymesh Confidential Assets
#### DART (original research)
- An account-based confidential payments model that targets a large anonymity set with constant-size on-chain proofs.
- Uses commitments, accumulators (e.g., curve trees), nullifiers, and zero-knowledge proofs to update account states privately.
- Introduces receiver-side acceptance semantics and auditability concepts in an account model (as opposed to UTXO notes).
- Serves as the conceptual baseline for later Polymesh work.
#### P-DART — Polymesh “Decentralized, Anonymous, and Regulation-friendly Tokenization”
- Polymesh's evolution of DART to support regulated settlement on chain.
- Adds a settlement container with encrypted legs, explicit sender/receiver affirmations, and optional mediation.
- Defines compliance hooks (asset-specific auditors and mediators) with key-privacy goals for encryption.
- Details Proof of Balance approaches (auditor-assisted and generic) and the use of counters/counter-updates.
- Describes operational controls such as account freezing and forced transfer for exceptional scenarios.
:::info References
- P-DART paper: https://assets.polymesh.network/P-DART-v1.pdf
- DART paper: https://eprint.iacr.org/2025/239
:::
### Settlement
A container for one or more **legs** that will be executed under an affirmation workflow.
If you think in terms of transfers: a settlement executes one or more transfers; when there's just one leg, settlement ≈ transfer.
### Leg
A single transfer intent inside a settlement (sender, receiver, asset type, value), typically stored on-chain in encrypted form.
### Transfer
The conceptual movement of value from a sender to a receiver for a given asset. In this design, a transfer is represented as a **leg** within a **settlement**. When there's one leg, “settlement” and “transfer” are effectively the same operation from an end-user perspective.
### Settlement Creator
The party that creates a settlement and publishes the encrypted legs and a settlement proof.
### Affirmation
An explicit accept/reject decision recorded on-chain by a counterparty (sender/receiver) or by a mediator when required.
In the implementation you'll see proof types like `SenderAffirmationProof`, `ReceiverAffirmationProof`, and `MediatorAffirmationProof`.
### Claim
A receiver-side step that credits value into the receiver's finalized balance (and updates counters accordingly).
In the implementation this corresponds to a `ReceiverClaimProof`.
## Advanced Systems
### Zero-Knowledge Proofs (ZKP)
Allow public verification of correctness without revealing secrets.
Examples:
- “This account state transition is well-formed.”
- “The sender can afford this debit.”
- “This encrypted leg corresponds to a valid settlement leg.”
### Curve Trees / Accumulators
Compactly represent a set of valid commitments and support **membership proofs**.
In Polymesh you'll see curve-tree roots for account/asset state, updated as leaves are added.
### Relayer
Third-party broadcaster that submits confidential transactions on behalf of users. Verifies anonymous fee proofs first, then signs and broadcasts the transaction, and is reimbursed from the pooled fee account.
### Pooled System Fee Account
Shared on-chain POLYX pool used to decouple user identity from fee payment. Deposits are linked to confidential fee accounts, and reimbursements to relayers come from this pool without revealing which user funded them.
### Anonymous Fee Proof
Zero-knowledge proof showing a confidential fee account holds enough hidden POLYX to cover network fee plus relayer commission, without revealing balance, deposit linkage, or account identity. Used with the relayer flow and the pooled fee account.
## Ledger Mechanics
### Account State Commitment
The on-chain representation of a private account state (e.g., balances/counters) committed under a commitment scheme.
### Nullifier
A public tag revealed when consuming an old account state, used to prevent re-use (double-spend) of that old state.
### Finalized Balance
The spendable balance for an asset.
### Pending Counter
A counter used to support receiver affirmation and PoB. Roughly: “how many legs are still in-flight and must be accounted for”.
### Counter Update
A transaction that reduces counters for finalized legs so PoB can be performed without forcing the prover to link a long history of finalized transactions.
## Architectural Concepts
### Account-Based Model
Balances update via account state transitions, not UTXO-style coins.
The target is full anonymity in an account model with constant-size on-chain proofs.
### Auditors & Mediators
- **Auditors:** Retrospective visibility; can decrypt asset-specific settlement payloads to inspect.
- **Mediators:** Prospective control; can be required to affirm or reject before settlement proceeds.
See [Compliance & Regulation](/confidential-assets/compliance-and-regulation).
---
## Confidential Assets(2)
Polymesh Confidential Assets (**PCA**) enable privacy-preserving transfers of digital assets while still supporting regulated-market workflows like **receiver affirmation**, **multi-leg settlement** (transfers), and **asset-specific compliance access**.
At a high level, participants submit **Zero-Knowledge Proofs** (**ZKP**) that a balance update is valid (e.g., “I can afford this debit”, “this state transition is well-formed”) without revealing the underlying amounts or linking the sender/receiver identities. Separately, encrypted settlement details can be made accessible to designated compliance participants.
:::info Network Availability
Confidential Assets is live on **Testnet** as of Polymesh v8. It is not yet available on **Mainnet**, and the pre-release interface may still change before it reaches Mainnet. Build and test against Testnet.
:::
:::note Reference
Polymesh Confidential Assets are the Polymesh implementation of the P-DART paper (the underlying protocol design), which builds on the DART paper.
- P-DART paper: https://assets.polymesh.network/P-DART-v1.pdf
- DART paper: https://eprint.iacr.org/2025/239
:::
## Why Confidential Assets?
The Confidential Assets functionality on Polymesh (PCA), powered by P-DART (Polymesh - Decentralized, Anonymous, and Regulation-Friendly Tokenization), delivers a unique combination of high-level privacy and essential regulatory compliance features, making it ideal for tokenized securities and regulated finance.
Key benefits and achievements:
- **Full Anonymity with Constant Transaction Size:** PCA provides **full anonymity and confidentiality** for balances, transaction values, asset types, and identities in an **account-based model**. Crucially, it achieves this privacy guarantee with a **constant transaction size** (`O(1)`), resolving an inherent limitation of many other partially anonymous account-based systems whose size grows with the anonymity set.
- **Dual Regulatory Compliance (Prospective and Retrospective):** PCA introduces a robust compliance framework that supports both passive auditing and active control.
- **Auditors:** Hidden, passive compliance entities designated by asset issuers who can **decrypt asset-specific transaction data** to provide **retrospective auditability** for regulatory reporting or cap table management. Do not gate settlement execution.
- **Mediators:** Active participants with the **same decryption capabilities as auditors** (can decrypt and inspect asset-specific settlement legs), but who **must also affirm or reject** pending transfers, providing **prospective regulatory control**. Settlement remains pending until mediators act when required.
- **Support for Complex Multi-Leg Settlement:** PCA includes a mechanism for **settlement creation**, allowing a creator to define the sender, receiver, asset type, and value for each leg (representing an asset transfer) of a proposed settlement.
- **Receiver Affirmation and Reversibility:** Transactions require **explicit affirmation or rejection** from all counterparties, aligning blockchain payments with regulatory requirements often seen in securities transfers. The system also supports **reversibility**, allowing the sender to reclaim unaffirmed funds, which prevents assets from being locked indefinitely.
- **Avoidance of Concurrency Issues:** The PCA design circumvents the **concurrency issues** common in other account-based anonymous payments, where changes to the account state (e.g., from an incoming transaction) could invalidate an outgoing transaction's Zero-Knowledge Proof.
- **Non-Interactivity:** The system supports **non-interactivity**, enabling senders to initiate payments to receivers who may be offline at the time of transfer.
- **Proof of Balance (PoB) Support:** PCA enables a Proof of Balance mechanism, allowing any verifier to request the exact balance of a specific asset from a user, accommodating the complexities of split balances caused by pending transactions requiring receiver affirmation.
- **Multi-Party Transaction (MPT) Efficiency:** PCA efficiently facilitates the transfer of a specific asset to multiple receivers, or the affirmation of receipts from multiple senders, within a single transaction by combining proofs generated by multiple parties in a single transaction, thereby increasing efficiency in privacy-preserving settings.
- **Support for Mandatory Regulatory Actions:** The protocol supports key regulatory procedures, including **account freezing** and **forced transfers**.
## What's Private vs. Public
The PCA protocol is fundamentally designed to achieve a difficult balance: ensuring **full anonymity** for participants and transactions while maintaining a publicly verifiable, regulatory-compliant ledger. The model is not about hiding the entire chain, but strategically concealing the sensitive elements within cryptographic shells (commitments and Zero-Knowledge Proofs).
The following sections detail what is kept **Private (Confidentiality)** versus what is published to the chain as **Public (Integrity and Verification)**.
### What is Kept Private (Confidential Information)
P-DART/PCA achieves **full anonymity** by ensuring that sensitive transactional data is only verifiable via cryptographic proofs, concealing the details of the transfer and the identity of the confidential account owners.
- **Account State Secrets:** The entirety of a user's internal account state is kept private. This includes the **account secret key** , the **finalized (spendable) balance**, the pending transaction **counter**, and the secret **randomness** values used to derive the nullifier and generate the commitment.
- **Transaction Value and Asset Type:** The actual **transferred value** between accounts and the specific **asset type** involved are concealed through commitment or encryption within the Zero-Knowledge Proof (ZKP).
- **Identity and Linkability:** The **identities/public keys** of the sender and receiver are concealed within the transaction. Crucially, the protocol guarantees **unlinkability** between a participant's successive account states, preventing tracing of a user's transaction history.
- **Auditor/Mediator Keys in Transactions:** While the **public encryption key** of the designated auditor/mediator is publicly known, the protocol requires that this key be **concealed** during the confidential transaction itself. This concealment is necessary to prevent revealing the underlying asset type, as the mapping between the asset and the auditor is publicly known.
- **Decoupled Signing Key:** The confidential account keys used to generate the ZKP are kept secret and are distinct from the standard [public keys](/getting-started/keys) used to sign and submit the transaction to the network. This ensures that the validity of the confidential transfer relies solely on the ZKP integrity, not the public signing key, though practical anonymity requires [additional considerations](#key-nuances-in-execution-and-fee-payment) to avoid linking the public signing key to the activity.
### What is Public (On-Chain Verification and Integrity)
For the blockchain to maintain integrity, prevent fraud, and facilitate regulation, certain cryptographic objects and metadata must be publicly recorded:
- **Commitments and Accumulator:** A cryptographic representation of the new account state, the **account commitment**, is publicly published after a transaction. These commitments are aggregated into a global, publicly maintained **Accumulator**.
- **Nullifiers (Double-Spending Tags):** To prevent an old account state from being spent twice, the unique, deterministic **nullifier** associated with the _spent_ account commitment is publicly revealed and recorded in the **List of Nullifiers**. The nullifier ensures that double-spending attempts fail validation.
- **Proof Validity:** The **Zero-Knowledge Proof** itself is public information, which allows any consensus member to execute the `Validate` function and verify the **validity of the underlying computation** without accessing the hidden transaction data.
- **Regulatory Lists:** The **Issuer-Asset-Auditor list (LIAA)** and the **Key-Asset list (LKA)** are publicly maintained lists used for compliance lookups and asset registration.
- **Settlement Status and Metadata:** The fact that a transaction occurred, its unique identifier, and the transition of its **settlement status** (e.g., from pending to finalized) are recorded publicly.
### Key Nuances in Execution and Fee Payment
#### Signing Key vs. Proof Validation
The transaction submitted to the Polymesh network must be signed by a standard Polymesh key. However, this key's role is typically limited to covering network fees and authorization for submission, **not** validating the integrity of the confidential transfer:
- **Decoupled Validation:** In PCA, validation relies on the **Zero-Knowledge Proof**. If the proofs provided by the counterparties (sender, receiver, mediator) are valid, the transaction is accepted, regardless of the submitter's specific public signing key.
- **Settlement Affirmation:** Settlement logic requires **explicit affirmations** proven by the ZKP, where the user that generates the proof proves ownership of the secret key corresponding to the confidential account, not the standard signing key used for network interaction.
#### Fee Mechanism and Privacy
While PCA ensures that asset details and account balances remain confidential, the use of **POLYX** for network fees presents a potential **privacy leakage risk**. Because POLYX transactions are public on the Polymesh ledger, the address paying the fee can often be linked to a user's identity.
To maintain full anonymity, users should consider the following options to decouple their identity from their confidential transactions:
- **One-Time Use Wallets (Large Pool Funding):** A user can generate a fresh wallet for a confidential transaction. To prevent this new wallet from being linked back to their main identity, it should be funded from a **large, mixed pool**, such as a withdrawal from a **Centralized Exchange (CEX)**. This breaks the on-chain link between the user's primary holdings and the fee-paying account.
- **Third-Party Relayer Services (Off-chain Payment):** Users can employ a **relayer** to submit transactions on their behalf. The relayer pays the on-chain POLYX fee using their own account. The user can then reimburse the relayer through a **separate, private channel** (such as an off-chain payment or a different cryptocurrency), ensuring no traceable POLYX movement appears on the Polymesh ledger.
- **Anonymous Fee mechanism:** For users who want to pay network fees directly on-chain, without managing additional keys, while maintaining anonymity, PCA provides an on-chain anonymous fee payment mechanism. Users fund a confidential fee account and generate Zero-Knowledge Proofs to authorize fee deductions from a pooled system account. This proof is then shared with a Relayer Service that submits the transaction on your behalf. For detailed mechanics, see [Anonymous Fees](./anonymous-fees).
- **Network-Level Anonymity:** Even with these fee strategies, "network leakage" (such as an IP address) can reveal a user's identity. For maximum privacy, these fee-hiding techniques should be used alongside network-layer protections like a **VPN or the Tor network**.
By using these methods, users ensure that the public requirement to pay for network resources does not undermine the private nature of their confidential asset transfers.
## Background: DART → PCA
PCA is the Polymesh implementation of **P-DART** (Polymesh - Decentralized, Anonymous, and Regulation-friendly Tokenization), which evolved from the **DART protocol** to address the specific settlement and compliance requirements of regulated financial markets.
### P-DART: An Evolution for Settlement and Compliance
While DART provides the cryptographic foundation for decentralized, anonymous accounts and transfers, **P-DART extends this model** to support multi-party, regulated settlement workflows:
- **Settlement Creation:** P-DART allows a creator to define complex settlements with multiple "legs," where each leg specifies a sender, receiver, asset type, and value. This enables multi-asset, multi-party transactions in a single atomic operation.
- **Explicit Counterparty Affirmations:** Settlements require active consent (affirmation or rejection) from all designated participants (sender, receiver, and optionally mediators) before execution. This aligns blockchain payments with real-world regulatory requirements, such as those in securities transfers.
- **Prospective Regulatory Control (Mediators):** Beyond DART's retrospective auditor model, P-DART introduces **mediators**: active participants who must explicitly affirm or reject a pending settlement. This enables real-time regulatory intervention when required by policy or law.
- **Operational Controls:** P-DART adds administrative tools like **account freezing** (to prevent updates) and **forced transfers** (for key loss recovery or clawbacks), essential for regulated asset lifecycles.
- **Anonymous Fee Payment:** P-DART incorporates a sophisticated **Relayer Flow** using confidential fee accounts and Zero-Knowledge Proofs, allowing users to pay network fees anonymously without compromising privacy or enabling DoS attacks.
In essence: **DART is the foundation for private accounts and transfers; P-DART is the framework for regulated, multi-party settlement and compliance.**
## Core Building Blocks (Concepts)
- **Commitments:** Hide an account state while binding it to a unique state update.
- **Curve trees / accumulators:** Allow a participant to prove “my committed state is in the set of valid states” without revealing which one.
- **Nullifiers:** A one-way tag that prevents the same old state from being used twice.
- **Zero-knowledge proofs:** Prove well-formed updates (including range/consistency checks) without revealing the secret state.
- **Auditors and mediators:** Asset-specific encrypted visibility and optional “must-affirm” workflow support.
## Compliance Roles: Auditors and Mediators
Confidential Assets introduce two hidden roles for regulated assets:
- **Auditors:** Can **decrypt asset-specific settlement payloads** to inspect transaction details (sender, receiver, asset, value) for retrospective review and reporting. **Do not gate settlement**, transfers execute regardless of auditor review.
- **Mediators:** Possess **all auditor capabilities** (can decrypt and inspect asset-specific legs), but **must also affirm or reject** pending settlements before they execute. Settlement remains pending until mediators act when required by asset policy. This provides **prospective regulatory control**.
This dual model lets issuers choose between passive auditability (auditors only) or active intervention (auditors + mediators) per asset type.
## Transfer/Settlement Workflow (Summary)
Confidential settlement is multi-step to support receiver affirmation and optional mediation:
1. **Create settlement:** Publish encrypted legs + a settlement proof.
2. **Affirmations:** Sender, receiver, and optionally mediator affirm or reject.
3. **Claim/finalization:** Receiver claims to credit value into finalized balance.
This separation enables “accept/reject first, credit later” semantics for regulated assets.
## Proof of Balance and Operational Controls
:::info Implementation Status
Proof of Balance, account freezing, and forced transfer features are **not yet available on Testnet**. Support for these capabilities will be added in future releases.
:::
- **Proof of Balance (PoB):** Prove an asset balance to a verifier without revealing history. Implemented via counters and optionally counter-update transactions.
- **Account freezing:** Gate or prevent updates in exceptional cases under authorization.
- **Forced transfer:** Support recovery/key-loss and clawback flows under policy.
## Learn More
- [Architecture](./architecture): components and data flow
- [Settlement Workflow](/confidential-assets/settlement-workflow): settlement lifecycle, affirmation, claim, reversals
- [Compliance & Regulation](./compliance-and-regulation): auditors/mediators, PoB, controls
- [Comparisons](./comparisons): how this differs from other privacy approaches
- [Glossary](./glossary): definitions and terminology
---
## Governance
## Overview
Polymesh includes on-chain governance to manage network upgrades, changing parameters of the network (e.g. slashing percentages), permissioning network roles and distributing treasury funds.
The governance system is designed to allow both community members and technical experts to collaborate on improving Polymesh and managing network changes.
There are three main groups of actors in Polymesh governance:
- POLYX token holders
- Committees (e.g. Technical Committee, Upgrade Committee)
- Governing Council
The governance process begins either via a community submitted proposal, which is then curated by other POLYX token holders, or a committee submitted proposal. In both cases the governing council is responsible for assessing the change and determining whether it should be ratified, rejected or temporarily skipped.
The governance system consists of POLYX token holders populating and curating a list of PIPs ordered by their importance to the community. The governing council will then consider this list of PIPs, in their curated order, on a periodic basis (e.g. once per month), ratifying, rejecting or skipping each PIP in order as appropriate.
In addition to the Governing Council there are other committees (e.g. Technical Committee) that can submit PIPs directly to the Governing Council for review, bypassing the community curation process. However generally we expect most PIPs to go through the usual flow of the community curation process, and for direct committee submitted proposals to be rare.
## Governance Flow Diagram

## Polymesh Improvement Proposals
Polymesh Improvement Proposals (PIPs) represent an on-chain action that the community or a committee is recommending to the Governing Council.
Polymesh Improvement Proposals (PIPs) represent any of change to the network, and can be created both by dedicated committees as well as any POLYX token holder and are actioned by the Governing Council.
A PIP is an on-chain dispatchable function w/ parameters alongside some metadata giving some additional context as to why the function should be called. For example, it may be a call to `system::set_code(new_binary)` which is linked to a Github PR describing the change.
These PIP dispatchables can only be executed by the Governing Council and not an individual user.
PIPs are also used to permission certain on-chain identities to have special privileged roles. This includes adding and removing permissioned operators and DID registrars.
Some common examples include:
- network upgrades
- treasury disbursement
- tokenomics parameter change
- adding new permissioned operators
- adding new permissioned DID registrars
## Signalling
It is important that PIPs are curated by community members (POLYX token holders) to provide feedback to the Governing Council on the relative importance to the community of different PIPs.
Any POLYX token holder, in addition to being able to submit a PIP for consideration, can signal their view on other community submitted PIPS, both positively or negatively, by bonding POLYX to the relevant PIP. The POLYX token holders signal is proportional to the amount of POLYX bonded by that community member.
When the Governing Council triggers a PIP snapshot, we take these signals and use them to order the community submitted PIPs for consideration by the Governing Council.
Token holders can unbond their POLYX and remove their support (or lack of support) from a PIP at any time. Bonding POLYX to signal on different PIPs does not prevent token holders from staking their POLYX.
## Committees
Polymesh governance includes committees. Each committee is a group of identities which a specific remit to manage certain types of network changes.
Each committee has a membership, with the current members being responsible for voting new members in or existing members out.
Associated with each committee is a voting threshold - for example, if the committee has 5 members, and a threshold of 50%, then at least 3 members of the committee must agree on an action before it is executed by the committee.
Committees can submit PIPs which are included and then put in front of the Governance Council for consideration. The Governing Council can choose to ratify, reject or ignore a committee submitted PIP.
Unlike community submitted PIPs, a committee submitted PIP is not subject to signalling from the community of POLYX token holders, and is always eligible for actioning by the Governing Council.
## Governing Council
The Polymesh Governing Council is ultimately responsible for actioning PIPs which were submitted either via the community curation process, or directly by one of the committees.
Any member of the governing council can trigger a PIP snapshot. The snapshot summarises the current list of PIPs (both from the community and committees) and orders community based PIPs based on their signal.
The Governing Council then meet to discuss the PIPs included in the snapshot. For community curated PIPs, the Governing Council must work through them based on their curated order, choosing whether to ratify, reject or skip each PIP in turn.
For PIPs which are skipped, we track how many times they have been skipped, and limit the Governing Council to skipping a particular PIP too many times. This is designed to give the Governing Council the flexibility they need to reasonable govern the evolution of the Polymesh network, as well as ensuring that community signalling of the relative importance of PIPs is closely considered by the Governing Council.
For PIPs submitted by committees rather than through the community curation process, the Governing Council is free to ratify or reject these PIPs in any order.
The Governance Council can be thought of as a multisig controlled by its members, and has an associated voting threshold that must be reached in order to execute an action through the Governing Council. This applies both for PIP management as well as some additional non-PIP related actions that the Governing Council may need to execute.
These non-PIP actions include managing the membership of the Governing Council itself, with existing members needing to agree and vote on the addition or removal of members, as well as changes to the voting structure, for example the voting threshold.
The Governing Council is also able to manage permissioned roles such as operator identities and DID registrars through governance actions.
## Release Coordinator
One member of the Governing Council is elected as the Release Coordinator. The role of the Release Coordinator is to schedule PIPs that have been ratified by the Governing Council.
Every ratified PIP has a default execution time, set as a specific amount of blocks from the time it was ratified. The Release Coordinator can re-schedule any PIP to a different execution block or choose to enact it immediately. This provides flexibility to coordinate the release of critical fixes, and ensure that any stakeholders in a particular PIP are coordinated and well prepared before the PIP is executed.
Release coordinators are elected by the governing council members, and must be a current member of the governing council.
---
## Polymesh Private
## Introduction
Polymesh Private is a private-permissioned version of the Polymesh blockchain designed for entities seeking enterprise-grade blockchain technology within controlled environments.
Polymesh Private is tailored for entities that require a secure platform for blockchain solutions and are wary of public data exposure. This is particularly valuable in jurisdictions lacking clear regulations on the use of public Distributed Ledger Technology (DLT) for regulated assets.
## Key Features
Polymesh Private allows entities to benefit from Polymesh's comprehensive features for tokenization with extra privacy and control. Sophisticated cryptography secures and maintains data onchain confidentially, mitigating concerns for regulatory compliance. Compatibility with the public network means users can benefit from its global community and access continuous updates, features, and tools. It also provides easy migration to the public network, future-proofing entities for adaptation to the evolving regulatory landscape.
Benefiting from ongoing development within the public Polymesh infrastructure by both Polymesh and the wider community, Polymesh Private users can seamlessly access continuous updates, features, and tools. As the chain operator, you retain full control over the adoption of on-chain upgrades, ensuring compatibility and stability between public and private instances.
## Deployment and Governance
Entities can form consortiums or independently operate Polymesh Private instances, deciding how they wish to configure their instances of Polymesh Private and deploying them within a private network (e.g., VPN) with restricted access.
The entity managing the chain retains control over on-chain governance, determining node operators and transaction participants. Governance actions can be managed through committee voting or via administrative keys/multisig wallets. The chain operator also retains full control over the adoption of on-chain upgrades.
## Network tokens
Polymesh Private eliminates the need for publicly traded utility tokens, reducing uncertainty surrounding operating costs and token provenance. Transaction fees can be set to zero or managed using a native utility token for accounting and activity metering purposes, offering users flexibility in operational models.
---
## Developer Tooling
## Overview
Interacting with Polymesh (both Public and Private) is possible through several methods:
- Direct RPC access via HTTP or WebSocket.
- Accessing the chains API directly via the polkadot.js API.
- Utilizing the purpose-built Polymesh SDK (Typescript).
- Utilizing the Polymesh REST API (constructed with the Polymesh SDK).
- Utilizing the Chain indexer (based on SubQuery), which offers a PostgreSQL database and GraphQL API for accessing current state and historical data.
- Utilizing various other generic tools compatible with multiple Substrate-based blockchains.
## GitHub Repos
The Polymesh Private chain and associated tooling is available within the [Polymesh GitHub](https://github.com/PolymeshAssociation) organisation.
Each repository has its own README.md file to help get started with building and deploying the chain and each tool.
Please see repo licenses for details of usage restrictions.
### Development Environment
To ease the setup of a developer environment for Polymesh Private instances, we provide a Docker compose file that spins up a preconfigured Polymesh Private chain, and associated tooling.
The repository [https://github.com/PolymeshAssociation/Polymesh-private-dev-env](https://github.com/PolymeshAssociation/Polymesh-private-dev-env) provides the required docker files and configuration for this.
This environment includes:
- [A Polymesh Private Node](#polymesh-private-chain)
- [REST API](#polymesh-rest-api)
- [SubQuery Chain Indexer and GraphQL API](#polymesh-graphql-api)
- Hashicorp Vault (for managing signing keys)
Refer to this projects README for details on how to quickly launch an environment.
Alternatively you can set up and connect the various tools individually by referring to the individual repositories of those tools described in the following sections.
### Polymesh Private Chain
[https://github.com/PolymeshAssociation/polymesh-private](https://github.com/PolymeshAssociation/polymesh-private)
This repo is where you would source the binary required to run an instance of the Polymesh Private chain.
### Polymesh SDK
[https://github.com/PolymeshAssociation/polymesh-private-sdk](https://github.com/PolymeshAssociation/polymesh-private-sdk)
This repository contains our Polymesh SDK for Polymesh Private instances.
It is a Typescript based SDK providing purpose-built objects and APIs to interact with Polymesh.
The Polymesh SDK uses the polkadot.js API and the [Polymesh GraphQL API](#polymesh-graphql-api) to source data required for various SDK functionality. For a methods marked as using middleware in the SDK documentation an instance of the Polymesh GraphQL API is required to be connected.
The Polymesh SDK uses a Signing Manager ([https://github.com/PolymeshAssociation/signing-managers](https://github.com/PolymeshAssociation/signing-managers)) to manage keys required to sign and submit messages to the Polymesh chain. Signing Managers are pluggable components allowing a variety of approaches for key management and security, including local storage for development and Hashicorp Vault for production grade security. Signing Managers can also be used to integrate with third-party key custodians such as Fireblocks.
### Polymesh REST API
[https://github.com/PolymeshAssociation/polymesh-private-rest-api](https://github.com/PolymeshAssociation/polymesh-private-rest-api)
This repository provides a REST API interface that under the covers uses the above Polymesh SDK to interact with a Polymesh Private chain instance.
### Polymesh GraphQL API
[https://github.com/PolymeshAssociation/polymesh-subquery](https://github.com/PolymeshAssociation/polymesh-subquery)
This repository provides a chain indexing service which will ingest the events and extrinsics in each observed Polymesh block, and build an indexed PostgreSQL database that tracks corresponding chain data.
Its GraphQL API can be used to query for historical or current chain data.
It is used by the Polymesh REST API to gather data required for certain transactions and getter APIs, so its endpoint must be passed as a parameter when starting an instance of the Polymesh REST API.
## Tool Usage
The above tools are designed to be used seamlessly together. The Polymesh SDK uses the GraphQL API to gather chain data as needed, and the Polymesh REST API uses the Polymesh SDK to allow users to easily interact with the chain. It is also possible to use the GraphQL API as a standalone service, rather than only through the REST API and Polymesh SDK.
```mermaid
flowchart TB
U((User))
R[[Polymesh REST API Server]]
S(Polymesh SDK)
SM{{Signing Manager}}
P([Polymesh Private Chain])
GQL(Polymesh Private GraphQL API)
PG[("Chain Indexer (PostgrSQL)")]
U <--> R
R <--> S
PG --> GQL
S <--> GQL
S <--> P
S <--> SM
```
---
## Glossary(2)
This glossary provides definitions for key terms and concepts used throughout the Polymesh documentation. Understanding these terms will help you better navigate the Polymesh ecosystem and its features.
## Identity & Keys
### Permissioned Identity
On Polymesh, keys that participate in identity and asset related transactions (excluding [Session Keys](#session-keys)) must be linked to an on-chain [DID](/identity/). A node operator's DID must be granted an additional role making it a permissioned identity. Only keys linked to this identity will be allowed to be [Stash keys](#stash).
### Primary Key
This key is a special key under the on-chain identity. It has additional capabilities of adding and removing [Secondary Keys](#secondary-key) from an identity. It cannot access POLYX on secondary keys. It is recommended to keep the Primary Key in a cold wallet and **not** use it as a [Stash key](#stash).
### Secondary Key
Secondary keys are authorized by a [Primary Key](#primary-key) to join an identity. For the purposes of staking/POLYX transfers, they are no different from primary keys. Secondary keys can be given permissions to perform specific on-chain actions (e.g., interact only with specific [assets](#asset), [portfolios](#portfolio), or transactions).
### Session Keys
Session keys are cryptographic keys used by the [Node Operator](#node-operator) to sign data necessary for consensus and block production. These keys are stored on the operator node itself and are essential for participating in the network as an operator. Although session keys do not hold any funds, they can be used to perform actions that could result in penalties, such as double signing (equivocation), so it is critical to keep these keys secure.
Session keys can either be generated offline and injected into the operator node or generated within the operator node by calling the appropriate RPC method. Once generated, session keys should be persisted. Session keys must either be stored within the client or mounted from secure storage via external methods.
### Multisig
A type of account or transaction that requires multiple signatures to authorize actions, increasing security and enabling shared control. See [Multisig](/identity/advanced/).
### Authorization
A mechanism to grant specific permissions to other identities or keys, such as joining an identity, managing [portfolios](#portfolio), or acting as an agent. See [Authorizations](/authorizations/).
### Authorization Request
A formal on-chain request to grant permissions or perform actions (such as adding secondary keys, rotating primary keys, or transferring assets) that must be accepted by the target party. Used throughout Polymesh for secure delegation and access control.
### Rotate Primary Key / Rotate Primary to Secondary
Authorization types for changing the primary key of an identity or converting the current primary key into a secondary key and replacing it with a new primary key. These operations require explicit authorization and acceptance.
### Agent / Agent Permissions
An entity or key authorized to act on behalf of an asset issuer or identity, with specific permissions. Agents can perform asset management, compliance, or operational tasks. See [Agent Permissions](/asset-agents).
### Portfolio Custody
The right to manage or control a portfolio on behalf of the beneficial owner, without transferring ownership. Enables delegation of asset management while retaining ownership. See [Portfolio Custody](/portfolios/custody/).
### Subsidized Account / Fee Relayer / Subsidizer
An account that pays transaction fees on behalf of another account (the subsidized), enabling fee management and improved user experience. The subsidizer sets an allowance and can manage or revoke the relationship. See [Subsidized Accounts](/accounts/subsidized/).
### Paying Identity
The identity responsible for paying transaction fees in multisig or subsidized account arrangements. In multisig, the creator's identity is initially the paying identity.
### Admin Identity (Multisig)
An identity with special privileges to manage multisig configuration and membership, such as adding or removing signers or changing required signatures.
### Proposal (Multisig)
A transaction or configuration change submitted for approval by multisig signers. Proposals require a minimum number of approvals to execute.
### Signer (Multisig)
An account authorized to approve transactions in a multisig arrangement. The set of signers and required signatures define the multisig's security model.
### Required Signatures (Multisig)
The minimum number of approvals needed to execute a multisig transaction. This value is set during multisig creation and can be changed by proposal.
### Off-chain Authorization
A method of granting permissions or performing actions by signing data off-chain, enabling certain operations to complete in a single transaction. Used for adding secondary keys and other authorization-driven workflows.
### Identity Freezing / Unfreezing
The ability to freeze or unfreeze all secondary keys of an identity for security purposes. Only the primary key can perform this action.
### Beneficial Ownership
The true ownership of assets or portfolios, even if custody or control is delegated to another party.
### Multisig Identity
The identity to which a multisig key is linked. Multisig keys must be associated with an identity to perform most operations.
### Smart Contract
A smart contract is a programmatic account deployed on Polymesh that enables custom, automated, or complex business logic to be executed on-chain. Smart contracts run on `pallet-revive`, executing on [PolkaVM](#polkavm), with EVM/Solidity compatibility supported as one interface into the same pallet. On Polymesh, a smart contract can serve as a primary or secondary key for an identity, allowing it to control assets, act as a custodian, or manage permissions. Beyond acting as a key, smart contracts can interact with native Polymesh modules, extend protocol functionality, and implement custom workflows such as decentralized exchanges, compliance logic, or asset management. Smart contracts interacting with assets must be attached to an identity (DID) to function. See [Smart Contracts](/development/smart-contracts/).
### Proposal Voting (Multisig)
The process by which multisig signers approve or reject proposals. A proposal is executed or rejected based on the collected votes.
---
## Staking & Node Operations
### Session
A fixed period with a constant set of operators. Operators can only join or leave the set at the start of a session. Sessions are used to rotate keys and update the active operator set.
### Node Operator
A permissioned entity that runs nodes to collect transactions and produce blocks. Node operators stake their own [POLYX](#polyx), earn rewards for honest behavior, and face [slashing](#slashing) if they misbehave. See [Node Operations](/developer-resources/validator-node-guide).
### Nominator
A POLYX holder who stakes tokens by nominating node operators they wish to support. Nominators earn a share of rewards tied to the operator(s) their tokens are assigned to but do not run nodes themselves.
### Stash
The stash address is the address that holds the [POLYX](#polyx) bonded by the operator and, optionally, receives operator rewards. This is recommended to be a cold wallet to minimize risk exposure of staked tokens. Stash keys can be either a [Primary Key](#primary-key) or a [Secondary Key](#secondary-key) but **must** be linked to the permissioned [DID](/identity/) of the operator.
Stash keys can:
- Bond POLYX
- Bond extra POLYX
- Manage bonded funds directly. New bonds are always managed by the stash key itself; a separate [Controller key](#controller) is a legacy pattern only, retained for stashes bonded before this changed.
### Controller
The controller key is used to manage bonded funds, vote with bonded funds, and perform similar on-chain actions. New bonds no longer support setting a separate controller — the stash key itself is always the controller. A separate controller key is a legacy pattern retained only for stashes bonded before this change; calling `set_controller` on such a stash resets its controller back to the stash key, it can no longer be pointed at a different key. Where a legacy separate controller is still in use, it is recommended to use a [multisig](#multisig) address or a supported hardware wallet for it, since these keys can hold funds and directly control bonded funds. Consider the controller key a semi-cold wallet.
Controller keys can:
- Set/update [Session Keys](#session-keys)
- Set commission and validate
- Set a destination for reward payments (e.g., stash with automatically stake, stash unstaked, controller, or other address)
- Commence unbonding of stash tokens
- Withdraw unbonded tokens after the 28-day waiting period to the stash address (i.e., the controller cannot transfer unbonded tokens from the stash)
- Rebond tokens which are in the process of unbonding
- "Chill" the node (i.e., gracefully stop validating after the last era the node was elected to ends)
- Purge session keys
### Bonding
The process of locking [POLYX](#polyx) for the purpose of staking. See [Staking](/polyx/staking/).
### Staking
Locking POLYX to support network security and earn rewards. See [Staking](/polyx/staking/).
### Unbonding
The process of reclaiming staked tokens after a waiting period.
### Era
A fixed time period (typically 24 hours on Polymesh Mainnet) during which the elected node operators remain active and after which rewards are distributed. Each era determines the operator and nominator sets and is the interval for reward distribution.
### Slashing
Mechanism by which staked tokens are lost if misbehavior occurs.
### Equivocation
Occurs when an operator node commits to two or more conflicting states (for example, by signing multiple blocks for the same slot). Equivocation is considered a serious protocol violation and can result in slashing of staked POLYX and removal from the operator set.
### Immortal Transaction
A transaction valid at any time, not restricted to a specific block or era. When using immortal transactions, care must be taken to ensure the account nonce is correct, as replay protection is not provided by block height or era.
### Warm Spare Node
A synced node configured and ready to replace an active operator node manually in the event of failure or maintenance. A warm spare node does not have session keys active until it is promoted to active status, helping to prevent equivocation penalties.
---
## Assets & Portfolios
### Asset
A digital representation of value on Polymesh. Assets are managed at the protocol level and can have compliance, settlement, and lifecycle rules enforced on-chain. Assets can be either [fungible](#fungible-token--asset) or [non-fungible](#non-fungible-token-nft). Each asset is assigned a unique Asset ID and may have a ticker and external identifiers (such as ISINs, CUSIPs, etc.). See [Assets](/core/assets/).
### Asset ID
A unique 16-byte identifier (UUID) assigned to each asset at creation. Used throughout the chain to reference and manage the asset.
### Ticker
A unique, human-readable symbol that can be registered and linked to an asset for discoverability. Tickers can be transferred and unlinked from assets. See [Ownership Transfer](/core/assets/ownership-transfer).
### Divisibility
Indicates whether a fungible asset can be divided into fractional units. Divisibility is set at creation, but an indivisible asset can later be made divisible (irreversible). All fungible assets use 6 decimals of precision on-chain.
### Issuance
The process of minting new tokens for an asset (fungible or non-fungible), increasing the total supply and assigning balances to portfolios. For fungible assets, issuance creates additional tokens; for NFTs, issuance creates new unique tokens within a collection. Performed by an agent of the asset using the `asset::issue` method.
### Redemption
The process of burning tokens from a portfolio, reducing both the total supply and the portfolio's balance. Applies to both fungible assets (removing tokens) and NFTs (removing unique tokens from a collection). Performed by an agent of the asset using the `asset::redeem` method.
### Checkpoint
A snapshot of all holder balances for a fungible asset at a specific point in time. Used for corporate actions, compliance, and regulatory reporting. See [Checkpoint Management](/corporate-actions/checkpoints).
### Transfer Restriction
Rules that limit the transfer of assets based on criteria such as maximum investor count, maximum ownership percentage, or claim-based restrictions. Enforced via the statistics pallet. See [Transfer Restrictions](/compliance/transfer-restrictions).
### NFT Collection
A group of non-fungible tokens (NFTs) managed under a single asset. Each collection is associated with a unique Asset ID and user-defined metadata. The collection defines the metadata keys that are required to be present for all individual NFTs within it. See [Non-Fungible Assets](/core/assets/nft).
### Collection Metadata Keys
Properties that must be defined for each NFT in a collection. Registered before collection creation and stored on-chain for transparency and smart contract use. See [Asset Metadata](/core/assets/metadata).
### Token URI
A standardized metadata key for NFTs, pointing to off-chain JSON metadata describing the token.
### Base Token URI
A collection-wide base URI for NFT token metadata, used to construct full metadata URLs for each token.
### Image URI
A standardized metadata key for NFTs, pointing to an off-chain image representing the token.
### Base Image URI
A collection-wide base URI for NFT images, used to construct full image URLs for each token.
### Asset Metadata
Additional information associated with an asset, such as regulatory details, classifications, investment terms, or external references. Metadata can be local (asset-specific) or global (standardized across assets). See [Asset Metadata](/core/assets/metadata).
### Local Metadata
Asset-specific metadata defined and managed by the asset issuer or authorized agents.
### Global Metadata
Standardized metadata definitions usable across all assets, defined through on-chain governance.
### Agent Group
A set of permissions assigned to agents for a specific asset. Each agent belongs to exactly one group per asset, and groups define what actions agents can perform. See [Asset Agents & Permissions](/asset-agents).
### Custom Agent Group
A user-defined group with specific permissions for agents on a per-asset basis. Created using the `externalAgents::create_group` method.
### Security Token Offering (STO)
A fundraising method where tokens representing ownership of an asset are sold to investors. Managed on-chain with automated settlement. See [Security Token Offerings](/sto).
### Fungible Token / Asset
A fungible token or asset on Polymesh is an interchangeable unit of value, such as a token or share, where each unit is identical and mutually substitutable. Fungible tokens are divisible and can be transferred, issued, or redeemed in fractional or whole amounts. See [Fungible Assets](/core/assets/fungible).
### Non-Fungible Token (NFT)
A non-fungible token (NFT) is a unique digital asset on Polymesh that represents ownership of a specific, individually distinguishable item or record. Unlike fungible tokens, each NFT has distinct characteristics or metadata and cannot be exchanged on a one-to-one basis with other tokens. NFTs are commonly used for assets such as collectibles, certificates, or unique financial instruments, and are managed within NFT collections that define required metadata keys for each token. See [Non-Fungible Assets](/core/assets/nft).
### Asset Holdings (AssetHolder)
The two ways an identity's assets can be held on Polymesh: in a [Portfolio](#portfolio), or directly on an [Account ID Balance](#account-id-balance). Represented on-chain by the `AssetHolder` type (`Account` or `Portfolio`), used throughout the Asset, NFT, and Settlement pallets. See [Asset Holdings](/asset-holdings/).
### Account ID Balance
Assets held directly on a signing key's account rather than in a portfolio, represented on-chain as `AssetHolder::Account`. Does not support custody or portfolio-level secondary-key permissions. See [Asset Holdings](/asset-holdings/). (See also: [Asset Holdings (AssetHolder)](#asset-holdings-assetholder))
### Portfolio
A logical grouping of assets owned by an identity, one of two ways an identity's assets can be held (see [Asset Holdings](#asset-holdings-assetholder)). Portfolios allow users to organize, segregate, and control digital assets with flexible permissions. Each identity has a default portfolio and can create additional numbered portfolios. See [Portfolios](/portfolios/).
### Default Portfolio
The primary portfolio associated with an identity, used as the default destination for asset transfers. Cannot be deleted or renamed.
### Portfolio Movement
The transfer of assets between portfolios under the same identity. Does not require settlement or compliance checks.
### Custody
The management and safekeeping of assets on behalf of an owner. Custody can be delegated to third parties, such as custodians, using Polymesh's portfolio and authorization features. See [Custody](/portfolios/custody/). (See also: [Portfolio Custody](#portfolio-custody))
---
## Compliance & Verification
### Compliance
Protocol-level rules that govern asset transfers and ownership. Compliance ensures only eligible identities can hold or transfer certain assets, based on rules set by the asset issuer. See [Compliance](/compliance/).
### CDD (Customer Due Diligence)
A legacy identity claim type and onboarding concept used in Polymesh versions prior to v8. From v8, DID registration is the functional onboarding requirement for identity and asset-related network actions. See [Identity Onboarding](/identity/onboarding/).
### KYC (Know Your Customer)
KYC is a regulatory process to verify the identity and suitability of participants. On Polymesh, having a DID does not by itself satisfy all regulatory KYC requirements for asset issuers. Asset issuers should assess and implement their own KYC procedures based on the type of asset and relevant jurisdiction. KYC claims can then be added to identities and used in conjunction with compliance rules as appropriate. On-chain, KYC is typically enforced via KYC claims, which can be referenced in compliance rules.
### Claim / Attestation
A statement made by one identity about another, such as a KYC or accreditation claim. Claims are used for compliance and can be attached to identities. See [Claims](/compliance#claims).
---
## Settlement & Instructions
### Settlement
The process of transferring assets between parties on-chain. Polymesh supports atomic, multi-asset, multi-party settlements via [instructions](#instruction). See [Settlement](/settlement/).
### Instruction
An atomic group of asset transfers (legs) between counterparties. Instructions must be affirmed by all parties and are settled atomically. See [Instructions](/settlement/).
### Leg
A single transfer of assets from one party to another within an instruction.
### Venue
A logical grouping of instructions, representing a marketplace or context for settlement. Venues define who can sign off-chain receipts and manage instructions. Asset issuers can restrict asset transfers so that they are only permitted via instructions linked to specific venues, enabling fine-grained control over where and how assets are settled. See [Venues](/settlement/venues/).
### Execution
The process by which a settled instruction is executed on-chain, resulting in the actual transfer of assets between parties. Execution can be automatic or manual, depending on the instruction type, and occurs after all required affirmations are received. An instruction can only be executed when all compliance rules and transfer restrictions for the involved assets and parties are satisfied. See [Settlement](/settlement/).
### Affirmation
The act of a counterparty approving an instruction before settlement.
---
## Corporate Actions
### Corporate Action
An on-chain event affecting asset holders, such as distributions, voting, or reorganizations. Polymesh automates corporate actions via protocol features. See [Corporate Actions](/corporate-actions/).
### Distribution
A type of corporate action where assets (e.g., dividends) are distributed to eligible holders. See [Distributions](/corporate-actions/distributions/).
### Ballot
A corporate action involving voting by asset holders. See [On-Chain Voting](/corporate-actions/on-chain-voting/).
---
## Governance
Governance refers to the on-chain mechanisms and processes for managing network upgrades, permissioning, and parameter changes. Governance is performed by the [Governing Council](#governing-council), committees, and POLYX token holders. See [Governance](/governance/).
### PIP (Polymesh Improvement Proposal)
A formal proposal for changes to the network, such as upgrades, parameter changes, or privileged actions. PIPs are curated by the community and committees, and are actioned by the [Governing Council](#governing-council). See [Governance](/governance/).
### Governing Council
The on-chain body responsible for executing privileged actions, including PIPs, network upgrades, and permissioning. The council acts as a multisig, with a voting threshold for actions. Members can add/remove council members and elect a [Release Coordinator](#release-coordinator). See [Governing Council](/governance/).
### Committee
A group with special roles in governance, such as the Technical Committee or Upgrade Committee. Committees can submit PIPs directly to the council and manage specific network changes. See [Committees](/governance/). (See also: [PIP (Polymesh Improvement Proposal)](#pip-polymesh-improvement-proposal))
### Release Coordinator
A member of the Governing Council elected to schedule and coordinate the execution of ratified PIPs. The Release Coordinator can reschedule or expedite PIP execution. See [Release Coordinator](/governance/).
---
## Technical & Infrastructure
### Substrate (Polkadot SDK)
Substrate is the blockchain framework that underpins the Polkadot SDK, developed by Parity. It is used as the foundation for Polymesh and provides modular components (pallets) and support for custom business logic. Official resources are now found at [Polkadot SDK](https://polkadot.com/platform/sdk/).
### Primitives
Core business logic and financial abstractions provided at the base layer of Polymesh, such as asset origination, settlement, and compliance. Primitives are implemented as pallets.
### Consensus (NPoS, BABE, GRANDPA)
Polymesh uses Nominated Proof-of-Stake (NPoS) for operator selection, BABE for block production, and GRANDPA for block finality. See [Consensus](/architecture/).
### NPoS (Nominated Proof-of-Stake)
A consensus mechanism where token holders nominate node operators by staking POLYX, and the most-backed operators are selected to produce blocks.
### BABE (Blind Assignment for Blockchain Extension)
The protocol used to determine which operator produces each block in Polymesh.
### GRANDPA (GHOST-based Recursive ANcestor Deriving Prefix Agreement)
The protocol used for rapid block finality in Polymesh, where operators vote on chains rather than individual blocks.
### PolkaVM
A RISC-V based execution engine used by `pallet-revive` to run Polymesh smart contracts. EVM/Solidity compatibility is supported as one interface into the same pallet, alongside native PolkaVM contracts. See [Smart Contracts](/development/smart-contracts/).
### pallet-revive
The smart contract pallet on Polymesh, providing PolkaVM contract execution with EVM compatibility (Solidity contracts, standard Ethereum JSON-RPC tooling via the `eth-rpc` proxy). See [Smart Contracts](/development/smart-contracts/).
### Identity
An on-chain entity identified by a DID that represents a person, organization, multisig, or smart contract on Polymesh. Keys are linked to an identity (primary and secondary) and act using that identity's permissions and claims for identity and asset related operations. Keys must complete DID onboarding before performing these regulated actions. See [Identity](/identity/) and [Identity Onboarding](/identity/onboarding/).
### POLYX
The native token of Polymesh, used for transaction fees, [staking](/polyx/staking/), and governance. See [POLYX](/polyx/).
### Middleware
Off-chain services (such as the Polymesh GraphQL API) that index and provide access to chain data for SDKs and applications.
### Chain Indexer (SubQuery)
A service that ingests Polymesh blocks and builds an indexed database for querying chain state and history, used by the GraphQL API.
### SDK
The [Polymesh SDK](https://github.com/PolymeshAssociation/polymesh-sdk) is a TypeScript library for interacting with the Polymesh blockchain. See [Developer Resources](/development).
### REST API
A service that exposes Polymesh functionality via HTTP endpoints, often used for integration with external systems.
### Extrinsic
An extrinsic is a piece of information (such as a transaction or an inherent) that comes from outside the blockchain and is included in a block. In Polymesh (and other Substrate-based chains), extrinsics are how users and external actors interact with the chain, such as submitting transactions or signaling intent. See also [Polkadot Glossary: Extrinsic](https://wiki.polkadot.network/general/glossary/#extrinsic).
### Pallet
A pallet is a modular component in the Substrate framework that implements specific blockchain functionality (such as balances, identity, or staking). Polymesh is built from multiple pallets, each responsible for a part of the chain's logic.
---
## Polymesh Private & Confidentiality
### Polymesh Private
A private, permissioned version of the Polymesh blockchain, designed for enterprise and consortium deployments in controlled environments. Polymesh Private offers enhanced privacy, customizable governance, and compatibility with public Polymesh features. See [Polymesh Private](/polymesh-private/).
### Confidential Asset
An asset type that supports confidential balances and transfers using zero-knowledge proofs, while still supporting regulated-market workflows such as receiver affirmation and asset-specific auditor/mediator compliance access. See [Confidential Assets](/confidential-assets/). (See also: [Zero-Knowledge Proof (ZKP)](#zero-knowledge-proof-zkp))
### Confidentiality
The ability to maintain privacy over certain transactions and balances, especially via confidential assets and zero-knowledge proofs. See [Confidential Assets](/confidential-assets/).
### Zero-Knowledge Proof (ZKP)
A cryptographic proof that allows one party to prove knowledge of a value (such as a confidential balance) without revealing the value itself. Used for confidential asset operations. (See also: [Confidential Asset](#confidential-asset))
---
## Core Concepts
This section introduces the foundational ideas and features that make Polymesh unique. Explore the topics below to understand how Polymesh is purpose-built for regulated markets:
- [**Architecture**](/architecture): Technical stack, consensus, and network design.
- [**POLYX**](/polyx): The native token for transaction fees, staking, and network operation.
- [**Identity**](/identity): On-chain identity is required for all identity and asset related participants, enabling compliance and secure access control. Basic POLYX transfers and staking (excluding validator activities) do not require a DID.
- [**Assets**](/core/assets): Issue and manage fungible and non-fungible assets, with protocol-level standardization and lifecycle management.
- [**Asset Holdings**](/asset-holdings): How identities hold assets — via portfolios or directly on an account.
- [**Compliance**](/compliance): Enforce rules for asset transfers and ownership at the protocol level, supporting regulatory requirements.
- [**Settlement (Transfers)**](/settlement): Transfer assets on-chain with atomic, multi-asset, and multi-party support.
- [**Confidential Assets**](/confidential-assets): Privacy-preserving asset transfers with zero-knowledge cryptography while maintaining regulatory compliance.
- [**Corporate Actions**](/corporate-actions): Automate distributions, voting, and other on-chain events related to assets.
- [**Portfolios**](/portfolios): Organize and segregate assets, assign custody, and manage permissions.
- [**Governance**](/governance): On-chain governance for managing network upgrades and permissions.
- [**Polymesh Private**](/polymesh-private): Private, permissioned deployments of the Polymesh blockchain.
---
## Local Development Environment
This guide walks you through setting up and using the [`polymesh-dev-env`](https://github.com/PolymeshAssociation/polymesh-dev-env) Docker Compose environment for local Polymesh development.
## 1. Introduction
This environment provides a self-contained Polymesh ecosystem running locally using Docker. It runs in one of two modes:
**Default mode** starts the core Polymesh stack:
- A Polymesh blockchain node running in development mode.
- Indexing services (SubQuery indexer plus a GraphQL server) for efficient data retrieval.
- Two instances of the Polymesh REST API — one using local keys, one using HashiCorp Vault.
- HashiCorp Vault for secure key management.
- Automated setup scripts for Vault initialisation and test account/identity creation.
**EVM tooling mode** (`--profile evm`) adds, on top of the above:
- `eth-rpc` — an Ethereum JSON-RPC proxy in front of the node, so standard Ethereum tooling (MetaMask, Foundry, Hardhat, ethers.js, viem) can talk to Polymesh's `pallet-revive` contracts.
- A [Blockscout](https://www.blockscout.com/) block explorer (backend API plus web frontend) with its own dedicated PostgreSQL and Redis services.
It's designed for developers who want to build applications, test smart contracts, or experiment with Polymesh features without needing to connect to public testnets or mainnet.
## 2. Prerequisites
- **Docker Desktop** or **Docker Engine + Compose V2**: Ensure Docker is installed, running, and accessible from your terminal. You can verify with `docker ps`. Download instructions: [https://docs.docker.com/get-docker/](https://docs.docker.com/get-docker/)
- **Resources**: An idle default stack uses roughly 2.5 GB of RAM and around 2 GB of images. Enabling EVM tooling adds four more containers (Blockscout backend and frontend, plus their own Postgres and Redis), pushing idle memory to around 3.5 GB and adding ~1.6 GB of images. Give Docker headroom above these figures — usage grows with chain and indexing activity.
- **(Optional) `jq`**: A command-line JSON processor. Useful for parsing `curl` responses from the REST API. Install it via your system's package manager (e.g. `brew install jq`, `apt install jq`).
- **(Optional) `curl`**: Command-line tool for making HTTP requests. Usually pre-installed on Linux/macOS.
- **(Optional, for EVM work) Ethereum tooling**: [Foundry](https://getfoundry.sh/) (`forge`, `cast`), [Hardhat](https://hardhat.org/), or MetaMask, depending on your workflow.
## 3. Getting Started
### 3.1 Clone the repository
```bash
git clone https://github.com/PolymeshAssociation/polymesh-dev-env.git
cd polymesh-dev-env
```
### 3.2 Choose an environment preset
The `envs/` directory holds presets that pin the Docker image versions and ports for each service:
| Preset | Chain image | Purpose |
| --------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `envs/8.0` | `polymesh:8.0.2-testnet-debian` | Pinned chain v8 release. **Recommended starting point** — includes the optional EVM variables. |
| `envs/latest` | `polymesh:latest-testnet-debian` | Floating `latest` tags for the newest builds. Used as the default by the helper scripts. |
| `envs/7.2` | `polymesh-arm64:7.2.0-testnet-debian` | Legacy non-EVM preset. Chain v7.2 has no `pallet-revive`, so EVM variables are omitted. |
| `envs/local` | `polymesh:8.0.2-testnet-debian` | Like `envs/8.0`, but expects a locally built `polymesh-rest-api:local` image. |
| `envs/template` | — | Full reference of every supported variable, including all optional EVM/Blockscout settings. Also linked as `.env.example`. |
### 3.3 Start the environment
The repository ships helper scripts that wrap `docker compose` and wait for initialisation to finish. This is the recommended way to start and stop the environment.
**Default mode (no EVM tooling):**
```bash
./scripts/start-env.sh --env-file envs/8.0
```
**With EVM tooling (Ethereum JSON-RPC and Blockscout):**
```bash
./scripts/start-env.sh --env-file envs/8.0 --profile evm
```
`start-env.sh` starts the services detached and then blocks until the `environment-ready` container reports the environment is fully initialised, so you can use it directly in scripts and CI.
Useful flags:
- `--env-file ` — which preset to use. **If omitted, both scripts default to `envs/latest`.** You can also set it via the `COMPOSE_ENV` environment variable.
- `--profile evm` — start the optional EVM tooling services. Can also be set via `COMPOSE_PROFILES`.
- `--pull [policy]` — pull images before starting. A bare `--pull` means `always`; any Docker Compose pull policy (`always`, `missing`, `never`, `build`) is accepted, or set `COMPOSE_PULL_POLICY`. Recommended when using a preset with floating tags such as `envs/latest`:
```bash
./scripts/start-env.sh --env-file envs/latest --pull always
```
Using docker compose directly instead of the helper scripts
Copy your chosen preset to `.env` and run Compose yourself:
```bash
cp envs/8.0 .env
docker compose up -d # default mode
docker compose --profile evm up -d # with EVM tooling
```
Alternatively, point Compose at the preset without copying it:
```bash
docker compose --env-file=envs/8.0 --profile evm up -d
```
Note that the EVM services are gated behind the `evm` Compose profile, so **`--profile evm` must be repeated on every command that targets them**, including `down`, `logs`, and `ps`. The helper scripts handle this for you.
The first time you run this, Docker downloads the necessary images, which may take several minutes depending on your connection. Subsequent starts are much faster.
### 3.4 Check environment status
`start-env.sh` already blocks until the environment is ready, so if you used it you can skip ahead. To watch the initialisation as it happens — or if you started the stack with plain Compose — the `environment-ready` service logs tell you when it's fully operational:
```bash
docker compose --env-file envs/8.0 logs -f environment-ready
```
:::note Every `docker compose` command needs the env file
The image names in `compose.yaml` come from variables in the env file, so any Compose command (`logs`, `ps`, `down`, …) fails with `has neither an image nor a build context specified` unless it can find them. Either pass `--env-file envs/8.0` every time, or copy the preset once with `cp envs/8.0 .env` and drop the flag. The examples below assume you've done one or the other.
:::
The environment performs several initialisation steps on the first run — Vault init and unseal, then on-chain account and identity creation. Wait for a message like this:
```text
************************************************************************************
*** Polymesh Environment Ready! (Total initialization time: XXs) ***
************************************************************************************
```
First-time initialisation typically takes a few minutes — most of it waiting for the indexer to become healthy and for the on-chain identities to be created. Press `Ctrl+C` to exit the logs view. On subsequent starts (after a `down` that preserved volumes) readiness is reported almost immediately.
## 4. Endpoint Reference
Default host ports for every exposed service:
| Service | Endpoint | Port variable | Notes |
| ----------------------------- | ----------------------- | ---------------------------------- | ----------------------------------------------------------- |
| Node (Substrate WebSocket) | `ws://localhost:9944` | `POLYMESH_CHAIN_WS_PORT` | Use this for the SDK, polkadot.js, and the Polymesh Portal. |
| Node (Substrate HTTP RPC) | `http://localhost:9933` | `POLYMESH_CHAIN_RPC_PORT` | Retained for legacy compatibility — see the note below. |
| Node (P2P) | `localhost:30333` | `POLYMESH_CHAIN_P2P_PORT` | Not needed for normal development. |
| SubQuery GraphQL (middleware) | `http://localhost:3000` | `POLYMESH_SUBQUERY_GRAPHQL_PORT` | GraphQL playground is enabled. |
| REST API — local signers | `http://localhost:3004` | `POLYMESH_REST_API_LOCAL_SM_PORT` | Swagger/OpenAPI UI served at the root path. |
| REST API — Vault signers | `http://localhost:3005` | `POLYMESH_REST_API_VAULT_SM_PORT` | Swagger/OpenAPI UI served at the root path. |
| HashiCorp Vault | `http://localhost:8200` | `VAULT_PORT` | Vault UI is enabled at `/ui`. |
| Ethereum JSON-RPC | `http://localhost:8545` | `POLYMESH_ETH_RPC_PORT` | `--profile evm` only. |
| Blockscout explorer UI | `http://localhost:4000` | `POLYMESH_BLOCKSCOUT_PORT` | `--profile evm` only. |
| Blockscout backend API | `http://localhost:4001` | `POLYMESH_BLOCKSCOUT_BACKEND_PORT` | `--profile evm` only. Health check: `/api/v2/stats`. |
:::note Port 9933 is a legacy alias, not a separate endpoint
Modern Substrate nodes serve HTTP and WebSocket RPC on a **single** port, and this node listens only on 9944. Compose maps host port 9933 to that same port so tooling and scripts that still assume the historical HTTP-RPC port keep working. Both host ports reach the same RPC server, and either accepts HTTP or WebSocket — there is nothing available on 9933 that isn't also on 9944. New code should just use 9944.
:::
:::tip Keep the Blockscout ports in sync
The Blockscout frontend is a browser app, so it calls the backend from the user's browser using `NEXT_PUBLIC_API_PORT`. Compose wires that to `POLYMESH_BLOCKSCOUT_BACKEND_PORT`, so if you change the backend port, the frontend follows automatically — but if you override the frontend variables yourself, keep the two aligned or the UI will fail to load data.
:::
## 5. Understanding the Core Services
Here's a breakdown of each service defined in `compose.yaml` and its role.
- **`polymesh-node`**:
- **Description**: The core Polymesh blockchain node, using the image specified by `POLYMESH_CHAIN_IMAGE`.
- **Why it's there**: This is the actual blockchain where transactions are processed and blocks are produced. It runs with `--dev`, which uses the development chain spec (pre-funding well-known accounts like Alice, Bob, and Charlie) and, combined with `--alice --validator --force-authoring`, produces blocks as a single authority. It also runs with `--pruning=archive` (so historical state stays available for the indexer), `--rpc-cors=all`, `--rpc-methods=unsafe`, and `--unsafe-rpc-external` — all appropriate for a throwaway local chain and **never** for a public node.
- **Ports**: WebSocket (`9944`), HTTP RPC (`9933`), and P2P (`30333`).
- **Data**: Persists chain data in the `chain-data` named volume.
- **`postgres`**:
- **Description**: A PostgreSQL 16.1 (Alpine) database instance.
- **Why it's there**: Required by the SubQuery indexer (`subquery-node`) to store indexed blockchain data. It also hosts a small `services_status` database used by the `vault-init` and `polymesh-rest-api-vault-sm-init` scripts to record their progress. The `psql_extensions.sql` script runs on initialisation to set up the database and required extensions.
- **Data**: Persists data in the `psql-data` named volume.
- **`subquery-node`**:
- **Description**: The Polymesh SubQuery indexer. It processes blocks from `polymesh-node` and stores relevant data in `postgres` according to a predefined schema.
- **Why it's there**: Provides an efficient way to query historical blockchain data (transactions, events, balances, and so on) without hitting the node directly, which is much slower for complex lookups. The REST API and SDK rely on it for historical queries.
- **Dependencies**: Starts after `postgres` and `polymesh-node` are healthy.
- **`subquery-graphql`**:
- **Description**: A GraphQL server exposing the data indexed by `subquery-node`.
- **Why it's there**: Offers a structured, powerful way to query indexed chain data. It is the "middleware" endpoint consumed by the Polymesh REST API, the SDK, and the Polymesh Portal.
- **Ports**: Exposes the GraphQL endpoint on port `3000` (configurable via `POLYMESH_SUBQUERY_GRAPHQL_PORT`), with the playground enabled.
- **`vault`**:
- **Description**: An instance of HashiCorp Vault, a tool for managing secrets and protecting sensitive data.
- **Why it's there**: Provides a secure way to manage the private keys used by one of the REST API instances (`polymesh-rest-api-vault-sm`), mimicking a production setup where keys aren't hardcoded or stored insecurely.
- **Ports**: Exposes the Vault API and UI on port `8200` (configurable via `VAULT_PORT`).
- **Data**: Persists data in the `vault-volume` named volume (logs in `vault-log-volume`). Configured via `scripts/vault-config.json`.
- **`vault-init`**:
- **Description**: A run-once service that executes `vault-init-dependencies.sh` (installing `bash`, `jq`, and `psql` into its temporary container) and then `vault-init.sh`.
- **Why it's there**: Automates the Vault setup. On the first run it initialises Vault with a single unseal key and root token (saved to the `vault-root-token` volume as `.unseal_key` and `.token`), unseals it, enables the `transit` secrets engine used for signing, and creates five ED25519 keys: `admin`, `signer1`, `signer2`, `signer3`, and `signer4`. On subsequent runs it simply unseals Vault using the saved key. It records its status in the `services_status` table in `postgres`.
- **Dependencies**: Runs after `vault` starts and `postgres` is healthy. The Vault REST API waits for this service to complete successfully.
- **`polymesh-rest-api-vault-sm`**:
- **Description**: An instance of the Polymesh REST API configured to use HashiCorp Vault as its **S**igning **M**anager (SM).
- **Why it's there**: Provides a standard HTTP API for interacting with the Polymesh node, using keys managed securely within Vault. The service automatically reads the `VAULT_TOKEN` from the `vault-root-token` volume (created by `vault-init`). Signer identifiers for this API use the format `{key_name}-{key_version}` (e.g. `admin-1`, `signer1-1`).
- **Ports**: Exposes the API on port `3005` (configurable via `POLYMESH_REST_API_VAULT_SM_PORT`).
- **Dependencies**: Starts after `polymesh-node`, `subquery-graphql`, and `vault-init` are ready.
- **`polymesh-rest-api-vault-sm-init`**:
- **Description**: A run-once service that executes the `rest-api-accounts-init.sh` script.
- **Why it's there**: Automates the setup of on-chain accounts and identities corresponding to the keys created in Vault. It waits for `polymesh-rest-api-vault-sm` to be healthy, then uses that API to:
1. Look up the Polymesh addresses for `admin-1`, `signer1-1`, …, `signer4-1`.
2. Call `/developer-testing/create-test-admins` to bootstrap `admin-1`: the chain's sudo key prefunds it, `admin-1` registers its own DID with `identity::self_register_did`, and sudo then tops it up to 20,000,000 POLYX.
3. Call `/developer-testing/create-test-accounts` (signing as `admin-1`) for `signer1-1`, …, `signer4-1`: `admin-1` prefunds each account, each one then registers its own DID with `identity::self_register_did`, and `admin-1` funds it to 100,000 POLYX.
4. Save the resulting addresses and DIDs to files in the `rest-api-accounts-init` volume for persistence checks.
5. Create a `.setup-complete` file in that volume to signal completion to `environment-ready`, and record its status in the `services_status` table.
- **Dependencies**: Runs after `polymesh-rest-api-vault-sm` is healthy.
- **`polymesh-rest-api-local-sm`**:
- **Description**: A _second_, independent instance of the Polymesh REST API configured to use the **Local** **S**igning **M**anager.
- **Why it's there**: Provides an alternative REST API endpoint that uses well-known development keys (Alice, Bob, Charlie) defined by their mnemonics (`//Alice`, and so on). This is simpler for basic tests or examples that rely on these known accounts, without involving Vault.
- **Ports**: Exposes the API on port `3004` (configurable via `POLYMESH_REST_API_LOCAL_SM_PORT`).
- **Dependencies**: Starts after `polymesh-node` and `subquery-graphql` are ready.
- **`environment-ready`**:
- **Description**: A lightweight container that waits for the `.setup-complete` file to appear in the `rest-api-accounts-init` volume (created by `polymesh-rest-api-vault-sm-init`).
- **Why it's there**: Provides a clear signal in the Docker logs that all automated initialisation steps have finished and the environment is ready for use — especially important on the first launch. `start-env.sh` waits on this container.
## 6. Understanding the EVM Tooling Services
These services only start when the `evm` Compose profile is enabled.
- **`polymesh-eth-rpc`**:
- **Description**: The `pallet-revive-eth-rpc` proxy (image configurable via `POLYMESH_ETH_RPC_IMAGE`), which translates standard Ethereum JSON-RPC calls into Substrate calls against `polymesh-node`.
- **Why it's there**: `pallet-revive` is a Substrate pallet, not a full Ethereum node, so it doesn't speak Ethereum JSON-RPC directly. This proxy lets MetaMask, Foundry, Hardhat, ethers.js, viem, and Blockscout target Polymesh unchanged.
- **Ports**: Exposes JSON-RPC on port `8545` (configurable via `POLYMESH_ETH_RPC_PORT`). It runs with `--rpc-cors=all` and `--allow-unprotected-txs` so legacy (non-EIP-155) transaction flows work for local experimentation.
- **Platform**: Pinned to `linux/amd64` by default (`POLYMESH_ETH_RPC_PLATFORM`); some upstream image tags are architecture-specific, so override this if you're on a different architecture.
- **`polymesh-blockscout`**:
- **Description**: The Blockscout explorer backend (indexer plus API), an Elixir/Phoenix application.
- **Why it's there**: Indexes the EVM view of the chain via `polymesh-eth-rpc` and serves the explorer API — block, transaction, address, contract, and token data — used by the frontend and available directly for scripting.
- **Ports**: Exposes the API on port `4001` (configurable via `POLYMESH_BLOCKSCOUT_BACKEND_PORT`).
- **Configuration**: Branded for Polymesh by default (`COIN`/`COIN_NAME` `POLYX`, `NETWORK` `Polymesh`, `SUBNETWORK` `Polymesh Dev`) and pointed at the local EVM chain ID (`POLYMESH_EVM_CHAIN_ID`, default `1641818`). Market data and Blockscout accounts are disabled since they're meaningless locally. `SECRET_KEY_BASE` has a throwaway default for local use — override it via `POLYMESH_BLOCKSCOUT_SECRET_KEY_BASE` for anything shared.
- **Data**: Persists logs and `dets` files in the `blockscout-logs` and `blockscout-dets` volumes.
- **`polymesh-blockscout-frontend`**:
- **Description**: The Blockscout web UI (a Next.js app).
- **Why it's there**: Gives you a familiar Etherscan-style browser interface over your local chain's EVM activity.
- **Ports**: Exposes the UI on port `4000` (configurable via `POLYMESH_BLOCKSCOUT_PORT`).
- **Wallet interaction**: The contract **Write** tab and MetaMask integration require a WalletConnect project ID. Set `POLYMESH_BLOCKSCOUT_WALLET_CONNECT_PROJECT_ID` (get a free ID from [cloud.reown.com](https://cloud.reown.com)) to enable in-browser wallet interaction. Read-only browsing works without it.
- **`blockscout-db`** and **`blockscout-redis`**:
- **Description**: A dedicated PostgreSQL 17 instance and a Redis instance for Blockscout.
- **Why they're there**: Blockscout keeps its own datastore, deliberately separate from the SubQuery `postgres` service so the two indexers can't interfere with each other. Redis backs Blockscout's rate limiting and caching.
- **Data**: Persist in the `blockscout-db-data` and `blockscout-redis-data` volumes.
## 7. Choosing a Signing Manager (Local vs Vault)
This environment runs two REST API instances, allowing you to choose how keys are managed:
- **Local signers (`polymesh-rest-api-local-sm` on port `3004`)**:
- Uses predefined development accounts (Alice, Bob, Charlie).
- Keys are derived from hardcoded mnemonics (`//Alice`, and so on).
- Simpler for basic testing or following SDK examples that use these accounts.
- Signer names are simple strings: `alice`, `bob`, `charlie`.
- **Vault signers (`polymesh-rest-api-vault-sm` on port `3005`)**:
- Uses keys securely stored and managed by HashiCorp Vault (`admin`, `signer1` through `signer4`).
- More closely mimics a production setup where keys are not exposed directly.
- Required for examples using the accounts automatically set up by `polymesh-rest-api-vault-sm-init`, which are the only accounts provisioned with on-chain identities and POLYX.
- Signer names follow the pattern `{key_name}-{key_version}`: `admin-1`, `signer1-1`, `signer2-1`, and so on.
You'll interact with one or the other depending on which accounts and keys you need. **The following examples primarily use the Vault SM (`localhost:3005`) because those accounts are automatically provisioned with identities.**
## 8. Interacting with the Environment
### 8.1 Browsing the REST API (OpenAPI/Swagger)
Both REST API instances serve their interactive OpenAPI UI at the root path, which is the fastest way to explore every available endpoint and payload shape:
- Vault signers: [`http://localhost:3005`](http://localhost:3005)
- Local signers: [`http://localhost:3004`](http://localhost:3004)
### 8.2 Connecting the Polymesh Portal
You can connect the Polymesh Portal to your local node for a visual interface:
1. Run the portal web UI locally. You can get it [here](https://github.com/PolymeshAssociation/polymesh-portal). Public portal instances are served over HTTPS, so browsers block them from connecting to an unsecured local WebSocket — running the portal locally avoids this mixed-content restriction.
2. Navigate to **Settings**.
3. Click the section displaying the current **Node RPC URL** and **Middleware URL**.
4. Choose **"Allow connecting to insecure local nodes"** if prompted or required by your browser.
5. Enter the following details:
- **Node RPC URL**: `ws://localhost:9944` (or your custom `POLYMESH_CHAIN_WS_PORT`)
- **Middleware URL**: `http://localhost:3000` (or your custom `POLYMESH_SUBQUERY_GRAPHQL_PORT`)
6. Save the settings. The portal should now reflect the state of your local chain.

To interact with the portal, add one of the built-in accounts to your wallet. Use the following development mnemonics to **restore with recovery phrase**:
- **bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice**
- **bottom drive obey lake curtain smoke basket hold race lonely fit walk//Bob**
- **bottom drive obey lake curtain smoke basket hold race lonely fit walk//Charlie**
and so on.
### 8.3 Using the REST APIs with `curl`
You can interact with the REST APIs directly using `curl` or any HTTP client. Remember to target the correct port (3004 for local SM, 3005 for Vault SM).
**Example: Get Alice's address (local SM)**
```bash
curl -s http://localhost:3004/signer/alice | jq
```
Expected output:
```json
{
"address": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
}
```
**Example: Get Signer1's address (Vault SM)**
The `rest-api-accounts-init.sh` script already did this and stored the result, but you can verify:
```bash
curl -s http://localhost:3005/signer/signer1-1 | jq
```
Expected output — Vault generates fresh keys, so **this address is different every time the environment is reset with its volumes removed**:
```json
{
"address": "5GKXbBc6Ha9YqgRAMH4NsAc1VGc7fb5mE34eD6Xtw6MNfJc6"
}
```
The addresses and DIDs created during initialisation are also printed in the init service logs:
```bash
docker compose logs polymesh-rest-api-vault-sm-init
```
**Example: Get Signer1's balance (Vault SM)**
```bash
SIGNER1_ADDRESS=$(curl -s http://localhost:3005/signer/signer1-1 | jq -r .address)
echo "Signer1 Address: $SIGNER1_ADDRESS"
# Now get the balance
curl -s "http://localhost:3005/accounts/${SIGNER1_ADDRESS}/balance" | jq
```
Expected output, reflecting the initial funding:
```json
{
"total": "100000",
"locked": "0",
"free": "100000"
}
```
### 8.4 Example: Transfer POLYX (Vault SM)
Let's transfer 50 POLYX from `signer1-1` to `signer2-1`.
1. **Get the addresses:**
```bash
SIGNER1_ADDRESS=$(curl -s http://localhost:3005/signer/signer1-1 | jq -r .address)
SIGNER2_ADDRESS=$(curl -s http://localhost:3005/signer/signer2-1 | jq -r .address)
echo "Signer1 Address: $SIGNER1_ADDRESS"
echo "Signer2 Address: $SIGNER2_ADDRESS"
```
2. **Prepare the transfer payload.** Supplying a `memo` makes this a `balances.transferWithMemo` extrinsic; the `signer` field identifies the Vault key that signs the request and pays the fee.
```bash
AMOUNT="50"
JSON_PAYLOAD=$(cat <12 scales Polymesh's 6-decimal POLYX up to the 18-decimal wei convention Ethereum tooling expects. So a balance of 1 POLYX appears as `1000000000000000000` over JSON-RPC. Configure wallets with 18 decimals.
:::
### 9.2 Smoke checks
Two scripts verify the EVM services are working:
```bash
./scripts/evm-smoke-test.sh
./scripts/blockscout-smoke-test.sh
```
`evm-smoke-test.sh` checks `eth_chainId` (asserting it matches `POLYMESH_EVM_CHAIN_ID`), `eth_getBalance`, and `eth_getLogs`:
```text
[EVM SMOKE] Using RPC endpoint: http://127.0.0.1:8545
[EVM SMOKE] Expecting chain id: 0x190d5a
[EVM SMOKE] eth_chainId OK: 0x190d5a
[EVM SMOKE] eth_getBalance OK: 0x18d0bf423c02f0095af000
[EVM SMOKE] eth_getLogs OK
[EVM SMOKE] All checks passed
```
`blockscout-smoke-test.sh` confirms the explorer backend answers with a valid payload on `/api/v2/stats`.
You can also query the endpoint by hand:
```bash
curl -s -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' \
http://127.0.0.1:8545 | jq
```
### 9.3 The pre-funded development accounts
The `--dev` chain spec pre-funds five well-known Ethereum-style accounts, immediately usable from Ethereum tooling — no `map_account` call needed, since they're funded directly at genesis to their `0xEE`-padded [fallback account](/development/smart-contracts#address-mapping):
| Name | Address | Private key |
| --------- | -------------------------------------------- | -------------------------------------------------------------------- |
| Alith | `0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac` | `0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133` |
| Baltathar | `0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0` | `0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b` |
| Charleth | `0x798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc` | `0x0b6e18cafb6ed99687ec547bd28139cafdd2bffe70e6b688025de6b445aa5c5b` |
| Dorothy | `0x773539d4Ac0e786233D90A233654ccEE26a613D9` | `0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68` |
| Ethan | `0xFf64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB` | `0x7dce9bc8babb68fec1409be38c8e1a52650206a7ed90ff956ae8a6d15eeaaef4` |
These are the same well-known Ethereum dev accounts — named to loosely pair with Alice, Bob, Charlie, Dave, and Eve, but each is an **independent secp256k1 keypair**.
:::danger These keys are public — never reuse them
They are well-known development keys published across Substrate/Frontier-ecosystem documentation. Anyone can spend from them. Use them only on throwaway local chains.
:::
To use **your own** Polymesh account from the EVM side instead, that account must first call `map_account` on `pallet-revive`. Until it does, funds sent to its derived Ethereum-style address land in a distinct `0xEE`-padded fallback account. Read [Address mapping](/development/smart-contracts#address-mapping) before doing this — it's the most common source of "missing" funds in EVM work on Polymesh.
### 9.4 Deploying a contract
Any standard Solidity toolchain works. Using [Foundry](https://getfoundry.sh/):
```bash
forge init counter && cd counter
```
Write a minimal contract at `src/Counter.sol`:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Counter {
uint256 public number;
function increment() public {
number++;
}
}
```
Deploy and interact with it against the local endpoint:
```bash
export ETH_RPC_URL=http://127.0.0.1:8545
export DEV_KEY=0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133
forge create --rpc-url $ETH_RPC_URL --private-key $DEV_KEY --broadcast src/Counter.sol:Counter
# Deployed to: 0x...
CONTRACT=0x... # address from the output above
cast call --rpc-url $ETH_RPC_URL $CONTRACT 'number()(uint256)'
cast send --rpc-url $ETH_RPC_URL --private-key $DEV_KEY $CONTRACT 'increment()'
cast call --rpc-url $ETH_RPC_URL $CONTRACT 'number()(uint256)'
```
Both EIP-1559 and legacy (type-0) transactions are accepted, the latter because `eth-rpc` runs with `--allow-unprotected-txs`.
For Hardhat, add a network pointing at the same values:
```js title="hardhat.config.js"
networks: {
polymeshDev: {
url: 'http://127.0.0.1:8545',
chainId: 1641818,
accounts: ['0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133'],
},
}
```
### 9.5 Using the Blockscout explorer
Open [`http://localhost:4000`](http://localhost:4000) to browse blocks, transactions, addresses, and contracts. The explorer is branded for Polymesh and denominated in POLYX out of the box:

The transaction list shows the EVM activity on your local chain, with each transaction classified by type — plain POLYX transfers, contract calls, and token transfers:

The backend API is directly usable too:
```bash
# Overall chain statistics
curl -s http://127.0.0.1:4001/api/v2/stats | jq
# Look up a deployed contract
curl -s "http://127.0.0.1:4001/api/v2/addresses/${CONTRACT}" | jq '{hash, is_contract}'
```
:::caution Blockscout only sees the EVM view of the chain
Blockscout indexes transactions submitted through the Ethereum JSON-RPC path. Native Substrate extrinsics — POLYX transfers via the REST API, asset creation, settlement, and everything else on the native layer — do not appear as transactions in Blockscout, even though their blocks do. For the native view, use the SubQuery GraphQL endpoint or the Polymesh Portal.
:::
Enabling the contract **Write** tab and MetaMask interaction in the Blockscout UI requires a WalletConnect project ID. Get a free one from [cloud.reown.com](https://cloud.reown.com) and set it in your env file:
```bash
POLYMESH_BLOCKSCOUT_WALLET_CONNECT_PROJECT_ID=your_project_id
```
### 9.6 EVM notes and caveats
- JSON-RPC method and subscription coverage varies by `eth-rpc` image tag. Keep `POLYMESH_ETH_RPC_IMAGE` pinned to a known-good tag rather than a floating one.
- Some upstream `paritypr` tags are architecture-specific; override `POLYMESH_ETH_RPC_PLATFORM` if the default `linux/amd64` doesn't suit your machine.
- Blockscout backend startup takes noticeably longer than the rest of the stack (it runs database migrations on first boot), so give it a minute after `environment-ready` reports success before expecting the UI to load.
- Polymesh's `pallet-revive` is the same upstream pallet used across the Polkadot ecosystem, so [Polkadot's smart contracts documentation](https://docs.polkadot.com/smart-contracts/) is a good reference for general EVM tooling — including its [JSON-RPC API guide for Ethereum developers](https://docs.polkadot.com/smart-contracts/for-eth-devs/json-rpc-apis/). Treat Polymesh-specific runtime settings (chain IDs, precompiles, address mapping) as documented in [Smart Contracts](/development/smart-contracts).
## 10. Stopping and Cleaning Up
The `stop-env.sh` helper always tears down the EVM services as well, so you never need to repeat `--profile evm` when stopping.
:::warning `stop-env.sh` removes your data by default
Unlike a plain `docker compose down`, `stop-env.sh` removes the named volumes (chain data, indexed data, Vault keys and accounts, Blockscout database) unless you pass `--keep-volumes`.
:::
**Stop and reset to a clean state** (the default):
```bash
./scripts/stop-env.sh --env-file envs/8.0
```
**Stop but keep the data** for the next start:
```bash
./scripts/stop-env.sh --env-file envs/8.0 --keep-volumes
```
Equivalent docker compose commands
Stop the containers without deleting data:
```bash
docker compose --profile evm down
```
Stop the containers and remove all associated volumes, resetting the environment:
```bash
docker compose --profile evm down --volumes
```
_Caution: this deletes all blockchain history, indexed data, Vault keys, and any accounts and assets created inside the Docker environment._
To apply a change to your env file, restart the environment: stop it (with `--keep-volumes` if you want to retain state), then start it again.
---
## REST API
## Overview
The Polymesh REST API provides a developer-friendly interface to interact with the Polymesh blockchain using standard HTTP requests. This API allows you to perform a wide range of operations without needing to interact directly with the lower-level blockchain protocols or the Polymesh SDK, making it accessible from virtually any programming language.
The API is open source and [available on GitHub](https://github.com/PolymeshAssociation/polymesh-rest-api/).
Built on Polymesh, an institutional-grade permissioned blockchain for regulated assets, the REST API offers endpoints covering core functionalities:
- **Identity Management**: Query identity details, associated keys, claims, and permissions. Register new identities (if authorized).
- **Asset Management**: Create, issue, manage, and query both fungible assets and non-fungible tokens (NFTs). Handle asset documents, ownership transfers, and metadata.
- **Compliance**: Define, manage, and query compliance rules, requirements, and trusted claim issuers for assets.
- **Settlement**: Create and manage settlement instructions and trading venues. Affirm, reject, and execute settlements.
- **Corporate Actions**: Initiate and manage dividend distributions, query corporate action configurations, and handle payments/claims.
- **Transactions**: Submit pre-signed transactions generated offline or trigger transactions signed by a configured Signing Manager (like HashiCorp Vault or local keys).
- **Network Information**: Query network status, block details, transaction history, and account balances.
- **Portfolio Management**: Create, manage, and query asset portfolios associated with identities.
- **Staking & Governance**: (Functionality may vary depending on API version) Interact with staking operations and governance proposals.
This API is ideal for developers building applications, backend integrations, or scripts that need to interface with Polymesh using familiar RESTful patterns.
:::note Try it Locally!
Want to run the examples below? Follow the [**Local Development Environment Setup Guide**](/development/tooling/local-setup) to run a complete Polymesh stack (node, indexer, REST APIs) using Docker on your machine. The local setup provides the REST API endpoints (e.g., `http://localhost:3005` for Vault-backed signing) used in the examples.
:::
## The Value of a Blockchain REST API
Providing a RESTful interface to a sophisticated blockchain like Polymesh offers distinct advantages. Traditionally, blockchain interaction requires specialized libraries (like SDKs specific to a language) or direct interaction with node RPC endpoints, potentially involving intricate data encoding and transaction construction.
A REST API simplifies this significantly:
1. **Accessibility**: Developers can use any programming language or tool capable of making HTTP requests (e.g., `curl`, Python `requests`, JavaScript `fetch`, Postman), eliminating the need to learn blockchain-specific libraries for basic interactions.
2. **Faster Integration**: Integrating Polymesh capabilities into existing applications (web services, enterprise systems) becomes much quicker, leveraging established patterns for API consumption.
3. **Reduced Complexity**: The API handles lower-level details like transaction formatting and interaction with the underlying SDK or node, presenting developers with clearer, resource-oriented endpoints.
4. **Standardization**: It follows familiar REST principles (standard HTTP methods, status codes, JSON request/response bodies), making it intuitive for a broad range of developers.
This approach allows developers to focus on their application logic rather than the intricacies of blockchain communication, accelerating development and broadening the potential ecosystem of integrated tools and services.
## Authentication
Secure endpoints typically require authentication, often via an API key provided in request headers (e.g., `x-api-key`). The API includes endpoints for managing these keys (`/auth/api-key/*`). Check the documentation for your specific API deployment regarding how to obtain and use authentication credentials. Public data endpoints might not require authentication.
## Signing Transactions
Many state-changing operations (POST, PUT, DELETE) require transaction signing. The REST API manages this through an `options` object in the request body, specifying the `signer` and `processMode`.
```json title="Example Options Object"
{
"options": {
"signer": "signer-name-or-address", // Identifies the key to sign with
"processMode": "submit" // Controls how the transaction is handled
}
// ... other request parameters
}
```
- **`signer`**: Specifies which key, managed by the API's configured Signing Manager (e.g., local mnemonic, Vault key like `signer1-1`), should authorize the transaction.
- **`processMode`**: Determines execution flow:
- `submit`: Sign, send, and wait for inclusion/failure.
- `offline`: Return the unsigned transaction payload for external signing.
- `submitWithCallback`: Submit and return immediately; results sent via webhook.
- `dryRun`: Simulate without submitting.
The API server uses its Signing Manager to perform the cryptographic signing based on the `signer` identifier.
## Key Features
The API provides endpoints grouped around core Polymesh functionalities:
- **Assets (`/assets/`)**: Create, issue, query, manage metadata, documents, ownership, compliance, and corporate actions for fungible assets.
- **NFTs (`/nfts/`)**: Manage NFT collections and individual non-fungible tokens.
- **Identities (`/identities/`)**: Manage on-chain identities, keys, permissions, claims, and related entities like portfolios and authorizations.
- **Accounts (`/accounts/`)**: Handle POLYX balances, transfers, transaction history, and account-specific permissions or subsidies.
- **Transactions (`/transactions/`)**: Submit externally signed transactions or query transaction status by hash.
- **Settlement (`/settlements/`, `/venues/`, `/instructions/`)**: Manage trading venues and the lifecycle of settlement instructions.
- **Compliance (`/compliance-requirements/`, `/trusted-claim-issuers/`)**: Configure and query asset compliance rules.
- **Corporate Actions (`/corporate-actions/`)**: Handle dividends, checkpoints, and other corporate events linked to assets.
- **Network (`/network/`)**: Retrieve blockchain status, block details, and configuration.
- **Signer (`/signer/`)**: Map configured signer names to their blockchain addresses.
- **Authentication (`/auth/`, `/users/`)**: Manage API access credentials (specific to deployment).
## Using the API with `curl`
These examples demonstrate basic interactions. Assume the API is running at `http://localhost:3005` (as provided by the local development setup using Vault). Set the `REST_API_BASE_URL` environment variable. Use `jq` to pretty-print JSON responses.
```bash
export REST_API_BASE_URL="http://localhost:3005"
# Optional: Set API key if needed
# export MY_API_KEY="your-api-key-here"
# Add header: -H "x-api-key: ${MY_API_KEY}" to curl commands if needed
```
### Example 1: Get Network Properties
Fetch basic information about the connected Polymesh network.
```bash
curl -s -X GET "${REST_API_BASE_URL}/network" \
-H "accept: application/json" | jq
```
**Example Response:**
```json
{
"name": "Development",
"version": "9",
"genesisHash": "0x..."
}
```
### Example 2: Get Asset Details
Fetch details for asset `MYASSET`. Replace with a valid ticker or Asset ID (e.g., one created via the local setup scripts or a previous API call).
```bash
export ASSET_ID="MYASSET" # Replace with an actual Ticker or Asset ID
curl -s -X GET "${REST_API_BASE_URL}/assets/${ASSET_ID}" \
-H "accept: application/json" | jq
```
**Example Response (structure):**
```json
{
"assetId": "unique-asset-id-string",
"ticker": "MYASSET",
"owner": "0x...",
"assetType": "EquityCommon",
"name": "My Example Asset",
"totalSupply": "1000000",
"isDivisible": true,
"securityIdentifiers": [
{
"type": "Isin",
"value": "US123..."
}
],
"fundingRound": "Series A",
"isFrozen": false,
"agents": ["0x..."]
}
```
### Example 3: Get Identity Details
Fetch details for an identity by DID. Replace `0x...` with a valid DID (e.g., one associated with `signer1-1` from the local setup).
```bash
# First get the DID for signer1-1 (assuming Vault setup)
SIGNER1_ADDR=$(curl -s -X GET "${REST_API_BASE_URL}/signer/signer1-1" -H "accept: application/json" | jq -r .address)
TARGET_DID=$(curl -s -X GET "${REST_API_BASE_URL}/accounts/${SIGNER1_ADDR}/identity" -H "accept: application/json" | jq -r .did)
echo "Querying DID: $TARGET_DID"
curl -s -X GET "${REST_API_BASE_URL}/identities/${TARGET_DID}" \
-H "accept: application/json" | jq
```
**Example Response (structure):**
```json
{
"did": "0x...",
"primaryAccount": {
"account": {
"signerType": "Account",
"address": "5..."
},
"permissions": {
/* ... full permissions ... */
}
},
"secondaryAccounts": [
// ... list of secondary accounts if any ...
],
"secondaryAccountsFrozen": false
}
```
### Example 4: Transfer POLYX
Transfers 50 POLYX from the account managed by `signer1-1` (Vault SM) to the account managed by `signer2-1`.
```bash
# Get recipient address (assuming signer2-1 is configured)
SIGNER2_ADDRESS=$(curl -s -X GET "${REST_API_BASE_URL}/signer/signer2-1" -H "accept: application/json" | jq -r .address)
if [ -z "$SIGNER2_ADDRESS" ] || [ "$SIGNER2_ADDRESS" == "null" ]; then
echo "Error: Could not resolve address for signer2-1. Make sure local dev setup completed."
else
echo "Recipient (signer2-1) Address: $SIGNER2_ADDRESS"
# Prepare JSON payload
JSON_PAYLOAD=$(cat < {
console.error('Failed to connect:', error);
process.exit(1);
});
```
## Relationship to Polymesh SDK
The primary [`@polymeshassociation/polymesh-sdk`](https://www.npmjs.com/package/@polymeshassociation/polymesh-sdk) package uses [`polymesh-types`](https://www.npmjs.com/package/@polymeshassociation/polymesh-types) internally. It provides higher-level abstractions (Entities like `Asset`, `Identity`, `Portfolio`; Namespaces like `assets`, `identities`) that simplify common operations and hide much of the direct `@polkadot/api` interaction.
If you are using the SDK, you benefit from the type safety provided by [`polymesh-types`](https://www.npmjs.com/package/@polymeshassociation/polymesh-types) without needing to manage the `typesBundle` or interact with `@polkadot/api` directly for most tasks. Stick to the SDK's methods unless you have a specific need for lower-level access.
## Links
* **GitHub Repository:** [`polymesh-types`](https://github.com/PolymeshAssociation/polymesh-types)
* **NPM Packages:**
* [`@polymeshassociation/polymesh-types`](https://www.npmjs.com/package/@polymeshassociation/polymesh-types)
* [`@polymeshassociation/polymesh-sdk`](https://www.npmjs.com/package/@polymeshassociation/polymesh-sdk)
* **Polymesh SDK Documentation:** [`/sdk-docs`](/sdk-docs)
* **`@polkadot/api` Official Documentation:** [`https://polkadot.js.org/docs/api/`](https://polkadot.js.org/docs/api/)
---
## Signing Managers
## Overview
Signing Managers are a crucial component of the Polymesh SDK, designed to abstract the complexities of cryptographic key management and transaction signing. They provide a flexible and secure way for applications to authorize transactions on the Polymesh blockchain without needing to handle private keys directly within the main application logic.
## Why Signing Managers?
Polymesh is built for regulated assets, demanding high standards of security and compliance. Signing Managers address several key challenges:
1. **Security:** They decouple sensitive key material and signing operations from the application core. This allows keys to be stored securely (e.g., in Hardware Security Modules (HSMs), dedicated key management services like HashiCorp Vault or Azure Key Vault, or browser extensions) instead of being hardcoded or stored insecurely within the application.
2. **Flexibility:** Applications can easily switch between different key management solutions (local development keys, browser wallets, enterprise-grade vaults) simply by configuring the appropriate Signing Manager, often with minimal code changes in the application itself.
3. **Institutional Workflows:** Enterprises often have strict policies around key management and transaction authorization. Signing Managers facilitate integration with these existing systems and workflows (e.g., requiring multiple approvals via the Approval Signing Manager).
4. **Compliance & Auditability:** Using specialized signing solutions (like HSMs via Fireblocks or Vault) can help meet regulatory requirements and provide clearer audit trails for transaction signing.
## How They Work
Conceptually, the SDK follows these steps when a transaction needs signing:
1. **Prepare Payload:** The SDK constructs the transaction data (the "payload") that needs to be signed.
2. **Delegate Signing:** It passes this payload to the configured Signing Manager instance.
3. **Obtain Signature:** The Signing Manager interacts with its underlying key storage (memory, browser extension, Vault, HSM, etc.) to perform the cryptographic signing operation using the appropriate private key.
4. **Return Signature:** The Signing Manager returns the generated signature to the SDK.
5. **Submit Transaction:** The SDK combines the original payload and the signature to form a valid, signed transaction and submits it to the Polymesh node.
This pluggable architecture allows developers to choose the key management solution that best fits their security requirements and operating environment.
## Available Signing Managers
Polymesh provides several official Signing Manager implementations:
---
### 1. Local Signing Manager
- **Package:** [`@polymeshassociation/local-signing-manager`](https://github.com/PolymeshAssociation/local-signing-manager)
- **Use Case:** Primarily for **development, testing, and simple scripts**. Ideal for getting started quickly and running examples.
- **Mechanism:** Stores BIP39 mnemonics or raw private keys **directly in memory** within the running application.
- **Security:** **Low.** Keys are exposed in the application's memory. **Not suitable for production environments or managing real assets.**
- **Environment:** Node.js
---
### 2. Browser Extension Signing Manager
- **Package:** [`@polymeshassociation/browser-extension-signing-manager`](https://github.com/PolymeshAssociation/browser-extension-signing-manager)
- **Use Case:** For building **web-based dApps** that interact with users' Polymesh Wallet browser extensions (or other compatible extensions like SubWallet, Talisman).
- **Mechanism:** Communicates with the browser extension via injected scripts. The extension manages the keys and prompts the user for authorization and signing.
- **Security:** **Medium-High.** Keys remain within the user's secure browser extension environment. Relies on the security of the user's extension and device.
- **Environment:** Browser
---
### 3. HashiCorp Vault Signing Manager
- **Package:** [`@polymeshassociation/hashicorp-vault-signing-manager`](https://github.com/PolymeshAssociation/hashicorp-vault-signing-manager)
- **Use Case:** For **server-side applications and enterprise environments** requiring secure, centralized key management using [HashiCorp Vault](https://www.vaultproject.io/).
- **Mechanism:** Interacts with a Vault instance via its API. Leverages Vault's `transit` secrets engine to perform signing operations without exposing private keys outside Vault.
- **Security:** **High.** Keys are managed within Vault's secure and auditable environment. Supports HSM integration via Vault Enterprise.
- **Environment:** Node.js
---
### 4. Azure Signing Manager
- **Package:** [`@polymeshassociation/azure-signing-manager`](https://github.com/PolymeshAssociation/azure-signing-manager)
- **Use Case:** For **server-side applications and enterprise environments** leveraging Microsoft Azure Key Vault for key management.
- **Mechanism:** Interacts with Azure Key Vault API to perform signing operations using keys stored within the vault.
- **Security:** **High.** Keys are managed within Azure's secure cloud infrastructure.
- **Environment:** Node.js
---
### 5. Fireblocks Signing Manager
- **Package:** [`@polymeshassociation/fireblocks-signing-manager`](https://github.com/PolymeshAssociation/fireblocks-signing-manager)
- **Use Case:** For **institutions using the Fireblocks platform** for digital asset custody and key management.
- **Mechanism:** Integrates with the Fireblocks API and MPC (Multi-Party Computation) signing process.
- **Security:** **Very High.** Leverages Fireblocks' MPC technology and institutional-grade security infrastructure.
- **Environment:** Node.js
---
### 6. Approval Signing Manager
- **Package:** [`@polymeshAssociation/approval-signing-manager`](https://github.com/PolymeshAssociation/approval-signing-manager)
- **Use Case:** Facilitating **workflows requiring explicit approval** before a transaction is signed. Useful for internal controls or integrating human approval steps.
- **Mechanism:** Acts as a wrapper around another Signing Manager. It intercepts signing requests, triggers an approval workflow (which needs to be implemented by the developer, e.g., via UI, email, internal system), and only proceeds with the actual signing (using the wrapped manager) upon receiving approval.
- **Security:** Depends on the wrapped Signing Manager and the implementation of the approval workflow. Adds a layer of operational control.
- **Environment:** Node.js
---
### 7. WalletConnect Signing Manager
- **Package:** [`@polymeshassociation/walletconnect-signing-manager`](https://github.com/PolymeshAssociation/walletconnect-signing-manager)
- **Use Case:** Enabling **dApps (web or potentially desktop/mobile)** to connect and request signatures from **mobile wallets** supporting WalletConnect v2 and the `polkadot_signTransaction` method on the Polymesh namespace.
- **Mechanism:** Uses the WalletConnect v2 protocol to relay signing requests to a compatible mobile wallet. The user approves the transaction on their mobile device.
- **Security:** **Medium-High.** Keys remain on the user's mobile device within their wallet app. Relies on the security of the mobile wallet and device.
- **Environment:** Browser, Node.js (depending on WalletConnect client implementation)
---
## Choosing the Right Manager
| Manager | Environment | Use Case | Security Level | Key Storage |
| :--------------------------------- | :--------------- | :--------------------------------------------- | :-------------------- | :-------------------- |
| **Local** | Node.js | Development, Testing, Simple Scripts | Low | In Memory |
| **Browser Extension** | Browser | Web dApps interacting with user wallets | Medium-High | Browser Extension |
| **HashiCorp Vault** | Node.js | Server-side, Enterprise, Centralized KMS | High | Vault (optional HSM) |
| **Azure** | Node.js | Server-side, Enterprise, Azure Cloud | High | Azure Key Vault |
| **Fireblocks** | Node.js | Institutional Custody with Fireblocks | Very High | Fireblocks MPC |
| **Approval** | Node.js | Workflows requiring explicit approval steps | Adds Operational Ctrl | Wraps another manager |
| **WalletConnect** | Browser/Node.js | dApps interacting with mobile wallets | Medium-High | Mobile Wallet |
## Basic Usage Example
### Local Signing Manager (Development)
```typescript
import { Polymesh } from '@polymeshassociation/polymesh-sdk';
import { LocalSigningManager } from '@polymeshassociation/local-signing-manager';
async function main() {
const signingManager = await LocalSigningManager.create({
accounts: [{ mnemonic: '//Alice' }], // WARNING: For dev/test only!
});
const sdk = await Polymesh.connect({
nodeUrl: 'wss://your-polymesh-node-url', // Replace with actual node URL
signingManager,
});
// Use the SDK...
const accounts = await signingManager.getAccounts();
const signingAccount = accounts[0];
console.log('Signing Account Address:', signingAccount);
// Example: Get balance
const account = await sdk.accountManagement.getAccount({ address: signingAccount });
const balance = await account.getBalance();
console.log('Balance:', balance.free.toString());
await sdk.disconnect();
}
main().catch(console.error);
```
## Offline Signing
For maximum security, especially in air-gapped environments, you might need to sign transactions offline. Signing Managers facilitate the *first* and *last* steps of this process:
1. **Generate Payload:** Use the SDK (with any configured Signing Manager, even a temporary local one) to prepare the transaction. Instead of calling `.run()`, call the `.toSignablePayload()` method on the transaction object. This returns the data that needs to be signed.
2. **Sign Offline:** Transfer this payload securely to your offline signing environment (e.g., air-gapped machine with keys, HSM). Use appropriate tools (like `polkadot-js/keyring` or hardware-specific tools) to sign the **raw payload** (`payload.rawPayload.data`) using the intended private key. Remember to prefix the signature correctly based on the key type (e.g., `0x00` for ed25519, `0x01` for sr25519) if the tool doesn't do it automatically.
3. **Submit Signed Transaction:** Bring the generated signature back to an online environment. Use the `sdk.network.submitTransaction()` method, providing the *original payload* (either `payload.payload` for JSON or `payload.rawPayload` for raw) and the hex-encoded *signature*.
The `offline-signing-examples` repository demonstrates this flow:
[`offline-signing-examples`](https://github.com/PolymeshAssociation/offline-signing-examples)
Signing Managers are **not** directly involved in the *actual offline signing step* (step 2), but they are essential for generating the correct payload (step 1) and can be used for submitting the pre-signed transaction (step 3, although `submitTransaction` doesn't strictly require a *signing* manager, just SDK connectivity).
## Security Considerations
- **Never expose production private keys or mnemonics** in your application code or insecure storage. Use secure Signing Managers like Vault, Azure, Fireblocks, or Browser Extension for production.
- **Secure API Keys/Tokens:** If using Vault, Azure, or Fireblocks, ensure the API credentials used by the Signing Manager are stored and accessed securely (e.g., environment variables, secrets management systems). Grant them least-privilege access.
- **Local Signing Manager:** Only use for development and testing with non-valuable keys.
- **Approval Signing Manager:** The security of the approval workflow itself is critical. Ensure it's robust and cannot be easily bypassed.
- **Regular Audits:** Periodically review your key management setup and signing workflows.
---
## Subscribing to Chain Events with @polkadot/api
## Introduction
While the [Polymesh SDK](./) provides high-level abstractions for most common blockchain interactions, there are scenarios where you might need lower-level access, particularly for listening to real-time events occurring on the chain. A prime example is subscribing to new blocks and processing the events they contain.
This guide demonstrates how to access the underlying [`@polkadot/api`](https://polkadot.js.org/docs/api/) instance, managed by the Polymesh SDK, to subscribe to finalized blocks and process specific events like POLYX transfers. Using the SDK's `@polkadot/api` instance ensures you have the correct Polymesh-specific type definitions, simplifying development and enhancing type safety.
:::warning Internal SDK Property
Accessing the `@polkadot/api` instance via `sdk._polkadotApi` uses an internal property of the SDK. While currently the standard way to achieve this for advanced use cases, be aware that internal details might change in future SDK versions. For most application logic, prefer the stable, high-level SDK methods.
:::
## Use Case: Monitoring POLYX Transfers
Imagine you need to build a service that monitors all POLYX transfers happening on the Polymesh network in real-time. This could be for analytics, notifications, or triggering other off-chain actions. The most efficient way to do this is by subscribing to finalized blocks and inspecting their events.
## Example Script Breakdown
The following sections break down a TypeScript script that connects to Polymesh via the SDK, accesses the underlying `@polkadot/api`, subscribes to finalized blocks, and logs details for any `balances.Transfer` events found.
### 1. Imports and Setup
First, we import necessary components from the SDK and its utilities:
```typescript
import { Polymesh } from '@polymeshassociation/polymesh-sdk';
import { UnsubCallback } from '@polymeshassociation/polymesh-sdk/types';
import {
balanceToBigNumber,
instructionMemoToString,
} from '@polymeshassociation/polymesh-sdk/utils/conversion';
// Node URL and global variables for SDK and API instances
const nodeUrl = 'ws://localhost:9944'; // Use your node's WebSocket endpoint
let sdk: Polymesh | null = null;
let api: Polymesh['_polkadotApi'] | null = null;
```
- `Polymesh`: The main SDK entry point.
- `UnsubCallback`: The type for the function returned when creating a subscription, used to unsubscribe later.
- `balanceToBigNumber`, `instructionMemoToString`: Utility functions from the SDK to convert chain data types (like `Balance` or `Memo`) into more usable formats (like `BigNumber` or `string`).
- We define the `nodeUrl` and initialize `sdk` and `api` variables.
### 2. Connecting and Accessing the API
The `main` function handles the connection and subscription setup:
```typescript
const main = async () => {
let unsubscribeFinalizedHeads: UnsubCallback | undefined;
try {
console.log('Connecting to Polymesh');
// Connect to the Polymesh blockchain using the SDK
sdk = await Polymesh.connect({
nodeUrl,
// Optional: suppress initialization warnings if needed
polkadot: { noInitWarn: true },
});
// Access the underlying @polkadot/api instance
// eslint-disable-next-line no-underscore-dangle
api = sdk._polkadotApi;
// Retrieve network properties to confirm a successful connection
const networkProps = await sdk.network.getNetworkProperties();
console.log(
'Successfully connected to',
networkProps.name,
'Spec version:',
networkProps.version.toString(),
);
// ... subscription logic comes next ...
} catch (error) {
console.error('Error:', error);
if (unsubscribeFinalizedHeads) {
unsubscribeFinalizedHeads(); // Clean up subscription on error
}
process.exit(1);
}
};
main();
```
- We connect using `Polymesh.connect`, passing the node URL.
- Crucially, we obtain the `@polkadot/api` instance using `sdk._polkadotApi`. This instance is already configured with all Polymesh-specific types.
- We fetch network properties using the SDK's `network.getNetworkProperties()` method to verify the connection.
### 3. Subscribing to Finalized Blocks
We use the accessed `api` object to subscribe to finalized block headers:
```typescript
// Inside the main function's try block:
unsubscribeFinalizedHeads = await api.rpc.chain.subscribeFinalizedHeads(
async (header) => {
// Callback function triggered for each finalized block
if (!api) throw new Error('API not initialized'); // Type guard
const blockHash = header.hash.toString();
// Process events within this block
await processBlockEvents(blockHash);
},
);
```
- `api.rpc.chain.subscribeFinalizedHeads` establishes a WebSocket subscription.
- It takes a callback function that executes every time a new block is finalized.
- The callback receives the block `header`. We extract the `blockHash` from it.
- We call a separate function, `processBlockEvents`, to handle the logic for fetching and parsing events for that specific block.
- The `subscribeFinalizedHeads` method returns an `UnsubCallback` function, which we store in `unsubscribeFinalizedHeads` to allow us to stop the subscription later.
### 4. Processing Block Events (`processBlockEvents`)
This asynchronous function takes a `blockHash` and processes all events within that block:
```typescript
/**
* Processes block events and handles Transfer events.
* @param hash The block hash for the finalized block.
*/
async function processBlockEvents(hash: string): Promise {
if (!api) throw new Error('API not initialized'); // Type guard
// 1. Get the full block data
const finalizedBlock = await api.rpc.chain.getBlock(hash);
const blockNumber = finalizedBlock.block.header.number.unwrap().toString();
const { extrinsics } = finalizedBlock.block; // Get extrinsics in the block
console.log(`Processing Block number ${blockNumber}`);
// 2. Get API instance at the specific block's state
const apiAtBlock = await api.at(hash);
// 3. Query all system events for this block
const events = await apiAtBlock.query.system.events();
// 4. Iterate through each event record
events.forEach((record) => {
// ... event processing logic ...
});
}
```
- **Get Block Data:** We fetch the full block using `api.rpc.chain.getBlock(hash)`. This gives us access to the block header (for the number) and the list of extrinsics included in the block.
- **API at Block State:** We get a specific instance of the API pinned to the state of the chain *at that block hash* using `api.at(hash)`. This is essential because chain state can change between blocks, and querying events requires looking at the state corresponding to the block they occurred in.
- **Query Events:** We use the block-specific API instance (`apiAtBlock`) to query all events recorded for that block via `apiAtBlock.query.system.events()`.
- **Iterate Events:** We loop through the array of event records.
### 5. Extracting Event and Extrinsic Details
Inside the `forEach` loop, we extract information about each event and, if applicable, the extrinsic that triggered it:
```typescript
// Inside the events.forEach loop:
if (!api) throw new Error('API not initialized'); // Type guard needed inside loop too
const { event, phase } = record;
let extrinsicHash: string | undefined;
let extrinsicId: number | undefined;
let method: string | undefined;
let section: string | undefined;
// Check if the event was emitted during an extrinsic's execution phase
if (phase.isApplyExtrinsic) {
extrinsicId = phase.asApplyExtrinsic.toNumber();
// Get the corresponding extrinsic from the block's list
const extrinsic = extrinsics[extrinsicId];
extrinsicHash = extrinsic.hash.toString();
method = extrinsic.method.method; // e.g., 'transferWithMemo'
section = extrinsic.method.section; // e.g., 'balances'
}
```
- Each `record` contains the `event` itself and the `phase` during which it occurred.
- The `phase` tells us if the event happened during the application of an extrinsic (`phase.isApplyExtrinsic`). If so, we can get the index (`extrinsicId`) of that extrinsic within the block's list.
- We use the `extrinsicId` to look up the actual extrinsic object from the `extrinsics` array obtained earlier.
- From the extrinsic, we can get its `hash`, `method` (the function name), and `section` (the pallet/module name).
### 6. Handling a Specific Event Type (`balances.Transfer`)
Now, we check if the current `event` is the one we're interested in – `balances.Transfer`:
```typescript
// Inside the events.forEach loop, after extracting phase info:
// Check if the event is a Balances.Transfer event
if (api.events.balances.Transfer.is(event)) {
// Extract data specific to the Transfer event
const balancesEvent = event.data;
const fromDid = balancesEvent[0].unwrapOrDefault().toString();
const fromAddress = balancesEvent[1].toString();
const toDid = balancesEvent[2].unwrapOrDefault().toString();
const toAddress = balancesEvent[3].toString();
const amount = balanceToBigNumber(balancesEvent[4]).toString();
const memo = balancesEvent[5].isSome
? instructionMemoToString(balancesEvent[5].unwrap())
: undefined;
// Log the details
console.log(
'Transfer event details:',
// Event-specific data:
{ fromDid, fromAddress, toDid, toAddress, amount, memo },
// Contextual data:
{ blockNumber, extrinsicId, extrinsicHash, section, method },
);
}
// ... could add else if blocks here to handle other event types ...
}); // End of events.forEach
```
- **Type Check:** `api.events.balances.Transfer.is(event)` is a type guard provided by `@polkadot/api` (aware of Polymesh types via the SDK's setup). It safely checks if the generic `event` object is specifically a `balances.Transfer` event.
- **Data Extraction:** If it is a `balances.Transfer` event, we access its specific payload via `event.data`. The indices (`balancesEvent[0]`, `balancesEvent[1]`, etc.) correspond to the fields defined for the `Transfer` event in the Polymesh runtime:
- `[0]`: `Option` (Sender DID)
- `[1]`: `AccountId` (Sender Address)
- `[2]`: `Option` (Receiver DID)
- `[3]`: `AccountId` (Receiver Address)
- `[4]`: `Balance` (Amount transferred)
- `[5]`: `Option` (Optional memo)
- **Data Conversion:** We use `.toString()` for addresses and DIDs. For complex types like `Balance` and `Memo`, we use the SDK's utility functions (`balanceToBigNumber`, `instructionMemoToString`) for safe conversion. We also handle `Option` types using `.unwrapOrDefault()` or `.isSome`/`.unwrap()`.
- **Logging:** Finally, we log the extracted transfer details along with the block number and extrinsic information for context.
### 7. Error Handling and Cleanup
The `main` function includes a `catch` block to handle potential errors during connection or subscription and, importantly, to clean up the subscription:
```typescript
// Inside the main function:
} catch (error) {
console.error('Error:', error);
// Ensure we unsubscribe if an error occurs after subscription starts
if (unsubscribeFinalizedHeads) {
unsubscribeFinalizedHeads();
}
process.exit(1); // Exit if connection/setup fails
}
// Note: In a real application, you'd also want to handle graceful shutdown
// (e.g., on SIGINT/SIGTERM) and call unsubscribeFinalizedHeads() there too.
```
- If any error occurs, it's logged.
- Crucially, `unsubscribeFinalizedHeads()` is called if the subscription was successfully established before the error occurred. This closes the WebSocket connection for the subscription and prevents resource leaks.
## Full Example Script
Here is the complete script combining all the parts:
```typescript title="subscribeEvents.ts"
import { Polymesh } from '@polymeshassociation/polymesh-sdk';
import { UnsubCallback } from '@polymeshassociation/polymesh-sdk/types';
import {
balanceToBigNumber,
instructionMemoToString,
} from '@polymeshassociation/polymesh-sdk/utils/conversion';
// --- Configuration ---
// Use the WebSocket endpoint of your target Polymesh node
// (e.g., public testnet, mainnet, or local node from polymesh-dev-env)
const nodeUrl = 'wss://testnet-rpc.polymesh.live'; // Example: Public Testnet
// const nodeUrl = 'ws://localhost:9944'; // Example: Local Node
// --- Global Variables ---
let sdk: Polymesh | null = null;
// Accessing the internal _polkadotApi property from the SDK instance
let api: Polymesh['_polkadotApi'] | null = null;
/**
* Processes block events and handles specific event types like Balances.Transfer.
* @param hash The block hash for the finalized block.
*/
async function processBlockEvents(hash: string): Promise {
if (!api) throw new Error('API not initialized');
try {
// Get the full block data using the block hash
const finalizedBlock = await api.rpc.chain.getBlock(hash);
if (!finalizedBlock) {
console.warn(`Block data not found for hash: ${hash}`);
return;
}
const blockNumber = finalizedBlock.block.header.number.unwrap().toString();
const { extrinsics } = finalizedBlock.block;
console.log(`--- Processing Block #${blockNumber} (${hash.substring(0, 10)}...) ---`);
// Get an API instance at the specific block's state for accurate event querying
const apiAtBlock = await api.at(hash);
// Query all system events that occurred in this block
const events = await apiAtBlock.query.system.events();
if (events.length === 0) {
console.log(' No events in this block.');
return;
}
// Iterate through each event record in the block
events.forEach((record) => {
if (!api) throw new Error('API not initialized'); // Check needed inside loop closure
const { event, phase } = record;
let extrinsicHash: string | undefined;
let extrinsicId: number | undefined;
let method: string | undefined;
let section: string | undefined;
// Check if the event was emitted during the application phase of an extrinsic
if (phase.isApplyExtrinsic) {
extrinsicId = phase.asApplyExtrinsic.toNumber();
// Retrieve the corresponding extrinsic from the block's list
const extrinsic = extrinsics[extrinsicId];
if (extrinsic) {
extrinsicHash = extrinsic.hash.toString();
method = extrinsic.method.method;
section = extrinsic.method.section;
}
}
// --- Handle Specific Event: balances.Transfer ---
if (api.events.balances.Transfer.is(event)) {
const balancesEvent = event.data;
// Extract data using indices corresponding to the event definition
const fromDid = balancesEvent[0].unwrapOrDefault().toString();
const fromAddress = balancesEvent[1].toString();
const toDid = balancesEvent[2].unwrapOrDefault().toString();
const toAddress = balancesEvent[3].toString();
// Use SDK utility to convert Balance type to BigNumber
const amount = balanceToBigNumber(balancesEvent[4]).toString();
// Use SDK utility to convert Option to string or undefined
const memo = balancesEvent[5].isSome
? instructionMemoToString(balancesEvent[5].unwrap())
: undefined;
console.log(' ✅ Balances.Transfer Event Found:');
console.log(` From DID: ${fromDid || 'N/A'}`);
console.log(` From Addr: ${fromAddress}`);
console.log(` To DID: ${toDid || 'N/A'}`);
console.log(` To Addr: ${toAddress}`);
console.log(` Amount: ${amount}`);
if (memo) {
console.log(` Memo: "${memo}"`);
}
if (extrinsicId !== undefined) {
console.log(` Context: Extrinsic #${extrinsicId} (${section}.${method}), Hash: ${extrinsicHash?.substring(0, 10)}...`);
}
}
// --- Add more 'else if' blocks here to handle other event types ---
// else if (api.events.asset.AssetBalanceUpdated.is(event)) {
// // Process AssetBalanceUpdated event...
// }
// else if (api.events.nft.NFTPortfolioUpdated.is(event)) {
// // Process NFTPortfolioUpdated event...
// }
});
} catch (error) {
console.error(`Error processing block ${hash}:`, error);
// Decide if the error is fatal or if processing can continue
}
}
/**
* Main function to connect to Polymesh and subscribe to finalized block headers.
*/
const main = async () => {
let unsubscribeFinalizedHeads: UnsubCallback | undefined;
try {
console.log(`Connecting to Polymesh node at ${nodeUrl}...`);
// Connect using the Polymesh SDK
sdk = await Polymesh.connect({
nodeUrl,
polkadot: { noInitWarn: true }, // Suppress warnings during init if desired
});
// Access the underlying @polkadot/api instance configured by the SDK
// eslint-disable-next-line no-underscore-dangle
api = sdk._polkadotApi;
// Verify connection by fetching network properties
const networkProps = await sdk.network.getNetworkProperties();
console.log(
'Successfully connected to:',
networkProps.name,
'| Spec version:',
networkProps.version.toString()
);
// Subscribe to finalized block headers
console.log('Subscribing to finalized blocks...');
unsubscribeFinalizedHeads = await api.rpc.chain.subscribeFinalizedHeads(
async (header) => {
const blockHash = header.hash.toString();
// Process events for the finalized block
await processBlockEvents(blockHash);
}
);
console.log('Subscription active. Waiting for new blocks... (Press Ctrl+C to exit)');
// Keep the script running until interrupted (e.g., Ctrl+C)
// In a real service, you might have a more robust way to handle lifecycle.
await new Promise(() => { /* Keep running indefinitely */ });
} catch (error) {
console.error('Initialization or subscription error:', error);
// Attempt to clean up the subscription if it was established
if (unsubscribeFinalizedHeads) {
console.log('Unsubscribing...');
unsubscribeFinalizedHeads();
}
// Disconnect SDK if initialized
if (sdk) {
await sdk.disconnect();
}
process.exit(1);
} finally {
// Ensure cleanup happens on exit signals (optional but good practice)
process.on('SIGINT', () => {
console.log('\nCaught interrupt signal (Ctrl+C). Cleaning up...');
if (unsubscribeFinalizedHeads) {
unsubscribeFinalizedHeads();
}
if (sdk) {
sdk.disconnect().then(() => process.exit(0));
} else {
process.exit(0);
}
});
}
};
// Run the main function
main();
```
## Conclusion
By accessing the `@polkadot/api` instance via `sdk._polkadotApi`, you can leverage the SDK's connection management and type definitions while performing lower-level tasks like subscribing to chain events. This approach provides a powerful way to build real-time monitoring and integration services for the Polymesh blockchain. Remember to handle subscription cleanup properly to avoid resource leaks. While effective for specific use cases like event subscription, always prefer the higher-level Polymesh SDK methods for standard application development tasks.
---
## Polymesh SDK
The Polymesh SDK is a TypeScript/JavaScript library that provides a simple, powerful interface for building applications and integrations on the Polymesh blockchain. It abstracts away blockchain complexity and exposes a feature-rich, user-friendly API for developers.
## Features
- High-level, type-safe API for interacting with Polymesh
- Supports asset creation, identity management, compliance, settlements, and more
- Works in Node.js and browser environments
- Flexible signing manager system for secure key management
- Extensive documentation and code examples
### 📖 [Full SDK API Reference](https://developers.polymesh.network/sdk-docs/)
## Getting Started
### 1. Install the SDK
```bash
npm install @polymeshassociation/polymesh-sdk
```
```bash
yarn add @polymeshassociation/polymesh-sdk
```
```bash
pnpm add @polymeshassociation/polymesh-sdk
```
> **Note:** It is highly recommended to use npm, yarn, or pnpm. Other package managers may not support all dependency overrides.
### 2. Initialize the SDK Client
You'll need a Polymesh node endpoint (e.g. `wss://mainnet-rpc.polymesh.network/`) and a [signing manager](/development/tooling/sdk/signing) if you wish to submit transactions.
```typescript
import { Polymesh } from '@polymeshassociation/polymesh-sdk';
import { LocalSigningManager } from '@polymeshassociation/local-signing-manager';
async function run() {
const signingManager = await LocalSigningManager.create({
accounts: [
{ mnemonic: '//Alice' },
// ...add more accounts as needed
],
});
const polyClient = await Polymesh.connect({
nodeUrl: 'wss://mainnet-rpc.polymesh.network/',
signingManager,
});
// Interact with the chain using polyClient
}
```
For browser apps, use the [Browser Extension Signing Manager](https://github.com/PolymeshAssociation/browser-extension-signing-manager) to connect to the Polymesh Wallet extension.
### 3. Example: Create an Asset
```typescript
const createAssetProc = await polyClient.assets.createAsset({
name: 'My Asset',
ticker: 'TICKER',
});
const newAsset = await createAssetProc.run();
```
### 4. Example: Read Data
```typescript
const assetsPage = await polyClient.assets.get({ size: new BigNumber(20) });
const asset = assetsPage.data[0];
const assetDetails = await asset.details();
console.log('asset details:', assetDetails);
```
---
## Related Pages
- [Signing Managers](./signing): Secure and flexible key management for the SDK
- [Polymesh Types](./types): Type definitions and low-level integration
- [SDK Examples Repository](https://github.com/PolymeshAssociation/polymesh-sdk-examples): Example scripts and usage patterns
- [Full SDK API Reference](https://developers.polymesh.network/sdk-docs/)
---
## Polymesh Tooling
Welcome to the Polymesh Tooling section! Here you'll find everything you need to build, test, and integrate with the Polymesh blockchain using a variety of tools and APIs.
## What You'll Find in This Section
- **Polymesh SDK:** TypeScript SDK for building robust blockchain applications and integrations. Includes guides for types, signing managers, and event subscriptions.
- **REST API:** A developer-friendly HTTP API for interacting with Polymesh from any language or platform.
- **SubQuery Indexer:** Efficiently query on-chain data using GraphQL, perfect for analytics, dashboards, and dApps.
- **Local Development Environment:** Run a full Polymesh stack locally with Docker Compose for rapid prototyping and testing.
- **Smart Contracts:** Learn about deploying and interacting with smart contracts on Polymesh using `pallet-revive` (a dual-VM engine supporting native PolkaVM and full EVM/Solidity compatibility) and native modules.
Each tool comes with detailed documentation, setup instructions, and practical examples to help you get started quickly.
## Quick Links
- [Polymesh SDK](./sdk/)
- [REST API](./rest-api/)
- [SubQuery Indexer](./subquery/)
- [Local Dev Environment](./local-setup/)
- [Smart Contracts](/development/smart-contracts/)
---
## Polymesh Node Operator Guide
## Introduction
Operators perform critical functions for the network and have strict uptime requirements. This document provides information about the recommended setup and maintenance of a Polymesh operator node. The intended audience is the operator's IT team; however, some business considerations have been included for completeness and to provide the necessary context.
## Governance
Polymesh is a permissioned network, meaning potential operators must undergo a governance process to be permitted to work with the Polymesh network. This governance process is on-chain and managed via the Polymesh Improvement Proposal (PIP) mechanism.
## Becoming an Operator
The node operator role on Polymesh requires a permission to be assigned. All operators must satisfy selection criteria determined by Polymesh and be approved by the Polymesh Governing Council.
To become an operator on Polymesh, you also need to bond (lock) POLYX in the system. This facilitates the economic incentives on which Polymesh's security relies. The account that stores your bonded POLYX is called the Stash account. New bonds no longer support pointing a separate Controller account at the stash — the Stash key itself always manages the bonded funds (see [Stash Key](#stash-key) below). Rewards generated for running an operator node can be sent to the Stash account or another specified account.
You do not need to bond all the POLYX in your Stash account and can bond more later. However, withdrawing any bonded POLYX requires waiting for the unbonding period, currently set at 28 days.
## Key Management
**NB**: It is recommended that operators use Ledger Nano S Plus, Nano X, Flex or Stax devices to store their keys. The discontinued Ledger Nano S should **NOT** be used.
The Nano S does not support setting session keys and is therefore unsuitable for operators.
There are two main types of keys that an operator must manage:
- Session keys
- Stash key
For new bonds, the Stash key itself performs all controller-type actions (see [Stash Key](#stash-key) below) — there is no separate Controller key to manage. Some stashes bonded before this change still use a legacy separate Controller key (see [Legacy Controller Key](#legacy-controller-key)).
The session keys are the only type of key that the operator node needs access to. The stash key (and any legacy controller key) should be securely stored in a supported hardware wallet.
### Session Keys
Session keys are used by the operator node to sign data necessary for consensus. These keys are stored on the operator node itself. Although session keys do not hold any funds, they can be used to perform actions that could result in penalties, such as double signing. It is therefore critical to keep these keys secure.
Session keys can either be generated offline and injected into the operator node or generated within the operator node by calling the appropriate RPC method. Once generated, session keys should be persisted.
Session keys must either be stored within the client or mounted from secure storage via external methods.
### Permissioned Identity
For validator node operations, stash keys must be linked to a DID that is assigned the operator role, making it a permissioned identity. Only keys linked to that permissioned identity can be used as Stash keys.
### Primary key
This key is a special key under the onchain identity. It has additional capabilities of adding and removing secondary keys from an identity. It cannot access POLYX on secondary keys. It is recommended to keep the Primary key as in a cold wallet and **NOT** use it as a stash key.
### Secondary keys
Secondary keys are authorized by a primary key to join an identity. For the purposes of staking/POLYX transfers they are no different to primary keys. Secondary keys can be given permissions to perform specific onchain actions. e.g. they may be restricted to specific transactions, to specific portfolios (which governs their ability to move and transfer assets), or to administering specific assets as an external agent.
### Stash Key
The stash key is the account that holds the POLYX bonded by the operator and, optionally, receives operator rewards. This should be a cold or semi-cold wallet and must never be attached to the operator node. It is recommended to use a multisig account or a supported hardware wallet for the stash key.
Stash keys can be either a primary or a secondary key but **MUST** be linked to the permissioned DID of the operator.
For new bonds, the stash key itself always manages the bonded funds — there is no separate controller to designate. The stash key can:
- Bond POLYX
- Bond extra POLYX
- Set/update session keys
- Set commission and validate
- Set a destination for reward payments - i.e. stash with automatically stake, stash unstaked, or other account.
- Commence unbonding of stash tokens
- Withdraw unbonded tokens after the 28 day waiting period to the stash account.
- Rebond tokens which are in the process of unbonding
- "Chill" the node i.e. gracefully stop validating after the last era the node was elected to ends.
- Purge Session keys
Because the stash key now directly performs these actions in addition to holding the bonded funds, securing it appropriately (hardware wallet or multisig) is especially important — treat it as at minimum a semi-cold wallet, not purely cold storage, since it needs to sign these operational transactions.
### Legacy Controller Key
Stashes bonded **before** this change (Polymesh v8) may still have a separate Controller key on file, which manages the bonded funds instead of the stash key performing these actions directly. This is a legacy pattern only — it cannot be set up for new bonds, and calling `set_controller` on such a stash resets its controller back to the stash key permanently (it can no longer be pointed at a different key afterwards).
Where a legacy separate controller is still in use, it should be treated the same way the stash key is treated above: use a multisig account or a supported hardware wallet, since it can hold funds and directly control bonded funds. This key is not directly required by the operator node and should never be shared with it.
Legacy controller keys can be a Primary or Secondary key and can be associated with any identity. They do not need to be associated with the permissioned identity of the Stash. Where a legacy controller exists, it (not the stash key) is the one that performs the actions listed above for the Stash Key.
## Network Architecture
The recommended secure operator setup for Testnet/Mainnet includes the following:
- An active operator node configured with session keys
- A warm spare operator node configured like an operator node but **without** session keys
A _minimum_ setup consists of a single operator node.
The `--reserved-only` flag, combined with the `--reserved-nodes` parameter, allows you to restrict connections to a whitelist of nodes that the operator node may peer with.
### Firewall Traffic
To function properly, Polymesh nodes require the following traffic to be whitelisted:
- **All Nodes**:
- **Port 443 Egress (HTTPS)** (optional but recommended): Used to send basic telemetry to Polymesh servers. Enabling telemetry allows your node to appear on the [Polymesh Telemetry page](https://stats.polymesh.network/).
- **NTP Egress**: System clock drift can cause a node to fail to produce blocks due to mismatched timestamps. Ensure your nodes are synchronized with a reliable NTP server.
- **Operator Nodes**:
- **Libp2p Ingress/Egress**: Operator nodes **must** send and receive P2P events from the WAN or a trusted set of nodes with WAN connectivity. _(Default Port: 30333)_
## High Availability
### Operator Node High Availability
The network tolerates simultaneous outages of multiple operator nodes, provided a critical threshold is maintained. An operator node can experience brief downtime for maintenance or upgrades. However, frequent or prolonged downtime—or outages of multiple nodes simultaneously—risks incurring slashing penalties.
**Only one operator node may be active with the same session keys at a time.** If multiple operator nodes with identical session keys are online simultaneously, they may sign conflicting blocks, resulting in equivocation penalties. Therefore, automatic failover is discouraged. Instead, maintain a warm spare node for supervised failover.
Two failover methods are available:
- **Shared Session Key**: The session keys are shared with the warm spare, which is activated if the primary node fails. The primary node **must not** come back online. **_The penalty for equivocation is much higher than for being offline._**
- **Unique Session Key**: Each operator node instance has a unique session key. If the primary node fails, the stash key (or legacy controller key, if one is in use) must update the session keys on-chain for the secondary node. As key changes take effect in the next session, there may still be a period where the node is offline for one session. This method eliminates the risk of equivocation penalties.
Storing the stash or controller key on a server for automated key change transactions is **not** recommended. However, you may pre-sign an immortal transaction (without a timeout) and store it on a server to broadcast if the primary node fails. See [Upgrading or Replacing a Node](#upgrading-or-replacing-a-node) for details.
## Getting the Polymesh Node Software
All Polymesh nodes use the same binary, differing only in runtime parameters.
There are several ways to obtain the node binary:
- **Prebuilt Container Images**: Fetch from the [Polymesh Docker Hub repository](https://hub.docker.com/r/polymeshassociation/polymesh). Two flavors are available: `debian` (with a shell for easier debugging) and `distroless` (reduced attack surface, no shell). Images are tagged as `` or `-`. Use versioned tags for deterministic updates. See our [sample Docker Compose files](https://github.com/PolymeshAssociation/polymesh-tools/tree/main/docker-compose). Refer to the Polymesh Developer Documentation for a guide to [running a Polymesh node with Docker](/node/docker).
- **Precompiled Binary**: Download from the [GitHub releases page](https://github.com/PolymeshAssociation/Polymesh/releases). Releases include the Polymesh binary, its checksum, and optional runtime archives. The runtimes are already included in the binary.
- **Build from Source**: Clone the [release branch](https://github.com/PolymeshAssociation/Polymesh/tree/mainnet) and follow the repository instructions to compile the binary.
## Node Resource Requirements
At the time of writing, each Polymesh node should have the following minimum resources:
| Resource | Minimum Value | Recommended Value |
| -------- | ------------- | ------------------------------------------ |
| CPU | 2 CPUs | 4 CPUs |
| RAM | 8 GB | 8+ GB |
| Storage | 250 GB SSD | 400+ GB low-latency SSD (e.g., local NVMe) |
As the blockchain grows, storage requirements will increase. Ensure sufficient spare storage or expandable volumes and monitor disk usage. A long-running node accumulates write-ahead logs (WAL) in the database directory. These logs are compacted upon node restart. Reserve an additional 40 GB of disk space for WAL.
Running multiple nodes on shared resources is not recommended. For example, it is preferable to run two 2-CPU/8-GB RAM instances (one node each) than a single 4-CPU/16-GB RAM instance hosting two nodes.
## Securing the Instances
Best practices for securing your instances should always be followed. These include (but are not limited to):
- Disabling password-based SSH access
- Setting up and enabling a network firewall
- Only opening required ports
- Disabling unnecessary services
- Avoiding the use of the root user and disabling root login
- Keeping your system up to date
- Enabling SELinux
- Monitoring logs and metrics for signs of malicious activity
- Running periodic CIS benchmarks against your systems
**Be advised:** Due to the constantly evolving cybersecurity landscape, the above list is not, and cannot be, comprehensive. Node operators are responsible for ensuring that their nodes remain secure and adhere to current best practices.
## Upgrading or Replacing a Node
### Operator Node Upgrades
The recommended process for upgrading operator nodes is to perform a failover to the warm spare operator node. As mentioned in the [High Availability](#high-availability) section, the unique key approach is preferable to the shared-key approach.
Begin by upgrading the warm spare operator node. Since this node is not actively validating, you can stop the Polymesh client, perform the necessary upgrade, and then resume operation.
Once the warm spare node is upgraded and fully synchronized, make it the active node by submitting a change of session keys associated with your stash to those stored in the warm spare's keystore, using your stash key (or legacy controller key, if one is in use).
To do so:
1. (If not already done) Generate a new set of session keys for the warm spare operator node.
2. Navigate to [Staking > Account Actions](https://mainnet-app.polymesh.network/#/staking/actions).
3. Click "Set Session Key" for your bonding account.
4. Enter the `keys` value from the warm spare node and provide the associated `proof`, then click "Set Session Key."
See [Running an Operator Node](#running-an-operator-node) for instructions on using the `author_rotateKeysWithOwner` RPC method to generate session keys and ownership proof.
The change in operator session keys will only take effect in the next session. **For safety, we recommend waiting at least two sessions before proceeding.** If the current session is `N`, wait until session `N + 2` before continuing.
At this point, the warm spare and active operator nodes will have switched roles: the previous warm spare becomes the active node, and vice versa. Ensure they are treated accordingly going forward. Alternatively, you may perform the failover operation again to restore the original active node as the current active node and the original warm spare as the current warm spare.
If a warm spare is unavailable, you may perform an in-place upgrade by stopping the Polymesh client, performing the necessary client upgrade, and restarting the node client. **We do not recommend in-place upgrades due to the risk of encountering a failure during the upgrade.**
## Backing Up a Node
Since Polymesh is a public blockchain, node backups are not strictly required, as you can always synchronize from scratch. However, syncing from scratch can be time-consuming. To minimize the delay between node creation and readiness, you may choose to back up the full blockchain database regularly.
This does not need to be done for every node—a single database backup can be used across nodes, provided they use the same pruning settings. In v8, all nodes including operators use the same default pruning: `--state-pruning 256` and `--blocks-pruning archive-canonical`. These defaults are appropriate for consensus participation. If you create backups with these settings, they can be shared across operator nodes.
Database backups should be performed on an offline node. A typical approach is:
1. Stop the Polymesh process on the backup node.
2. Snapshot the database directory.
3. Restart the Polymesh process.
4. Sync the database snapshot to offsite storage.
The database snapshot contains no confidential information as long as **only** the `db` directory is backed up (e.g., `//chains/mainnet/db`).
Because the database uses file-based storage, stopping/starting Polymesh may create partial files. Excessive file accumulation can cause performance issues. We recommend limiting snapshots to daily intervals and periodically resetting the backup node's database with a fresh sync from the chain.
## Auto-Restarting Nodes
Nodes should automatically restart in the event of an intermittent failure.
### Container-Based Nodes
For container-based nodes, use your container runtime's features:
- `restart_policy.condition: any` for `docker-compose`
- `restartPolicy: Always` for `kubernetes`
### Binary-Based Nodes
For binary-based nodes, we recommend using a supervisor process. Most modern Linux distributions use `systemd`, which we will focus on, though other options are also viable.
#### Setting Up `systemd`
1. Create a new unit file called `polymesh.service` in `/etc/systemd/system/` with the following content:
```ini
[Unit]
Description=Polymesh Node
After=network.target
[Service]
User=polymesh
Group=polymesh
Type=simple
Restart=always
RestartSec=5
ExecStart=/usr/local/bin/polymesh --chain mainnet --name --validator
[Install]
WantedBy=multi-user.target
```
2. Enable automatic startup with:
```bash
sudo systemctl enable polymesh
```
3. Manage the service with commands such as:
```bash
sudo systemctl start polymesh
sudo systemctl stop polymesh
sudo systemctl restart polymesh
```
4. View logs using `journalctl`:
```bash
sudo journalctl -u polymesh -f
```
Refer to the `journalctl` man pages for additional details.
## Common Parameters for Running a Polymesh Node
Recommended options for running a Polymesh node include:
- `--name ` (optional): Human-readable name reported to telemetry services.
- `--chain mainnet`: Run a Mainnet node (default is Testnet if omitted).
- `--wasm-execution compiled`: Use compiled WASM for better performance.
- `--base-path ` (optional): Specify the location for DB files and the keystore.
- `--node-key-file ` (optional): Use an explicit node key path when you manage node key outside the default "network" folder.
- `--db-cache ` (optional): Increase in-memory cache for better performance. On a node with 8GB of available RAM, a reasonable value is `4096`. (default `128` MiB)
- `--db-max-total-wal-size ` (optional): Limit the total storage capacity that the database can use for WAL files. Recommended minimum value `1024`.
- `--state-pruning` and `--blocks-pruning` (optional): Configure pruning behavior. Refer to [Configuring Pruning](/node/docker#configuring-pruning) for details. The v8 defaults (`--state-pruning 256` and `--blocks-pruning archive-canonical`) are appropriate for operator nodes.
**Note:** The `` parameter will be publicly visible when telemetry is enabled (default setting).
To see a full list of available options and their descriptions, run the `--help` command.
## Running an Operator Node
To run an operator node, use the following in addition to the [common parameters](#common-parameters-for-running-a-polymesh-node):
- `--validator`: Enable validator mode.
To connect only to trusted peers, use these options:
- `--reserved-only`: Restrict connections to reserved peers.
- `--reserved-nodes`: A space-separated list of libp2p peer addresses in the format `/ip4//tcp/30333/p2p/` or `/dns4//tcp/30333/p2p/`. If omitted, peers must be added via the `system_addReservedPeer` RPC method.
For validator operations, keep a stable node key persisted under your `--base-path`. With default settings, the node key is stored in the chain-specific network folder:
- `//chains//network/secret_ed25519`
If the key is stored in this default location, you do not need to pass `--node-key-file`.
Use `--node-key-file` when you intentionally store the node key in a custom location.
For testing you can avoid separate key generation by using `--unsafe-force-node-key-generation`. This forces key generation if the node-key-file does not exist on container start, which is useful for testing but not recommended for production.
`--unsafe-force-node-key-generation` is considered unsafe for active authorities because identity changes can break connectivity with peers that expect a stable peer ID.
### Generating and Inspecting Node Keys
Before starting a validator, generate and verify the node key explicitly.
Generate a node key in the default chain network location under your `--base-path`:
```bash
polymesh key generate-node-key --base-path --chain mainnet
```
With `--base-path ` and `--chain mainnet`, this writes the key to:
```plaintext
/chains/mainnet/network/secret_ed25519
```
If you run testnet, the path is:
```plaintext
/chains/testnet/network/secret_ed25519
```
Inspect the key and print the peer ID:
```bash
polymesh key inspect-node-key --file /chains/mainnet/network/secret_ed25519
```
If you keep the key in the default location, no explicit node-key argument is required at startup. Use `--node-key-file` only when you intentionally store the key in a custom path.
Next, generate the node's session keys.
### Generating Session Keys with Access to the Node's RPC Port
Use `author_rotateKeysWithOwner` to generate session keys and the required ownership proof in one call.
The `owner` parameter must be your **stash AccountId** in hex format: `0x` + the 32-byte raw public key bytes. An account transformation tool such as https://polymesh.subscan.io/tools/format_transform can be used to convert your stash account address to the required hex public key format.
```bash
curl -H "Content-Type: application/json" -d '{"id":1, "jsonrpc":"2.0", "method":"author_rotateKeysWithOwner", "params":[""]}' http://localhost:9944 | jq .result
```
You will get an output similar to:
```json
{
"keys": "0x4c28da...cfefe26",
"proof": "0x59e7e5...85901"
}
```
Record both values:
- `keys`: The concatenated **public** session keys.
- `proof`: The ownership proof required when setting/updating session keys on-chain.
Important behavior changes:
- Each call to `author_rotateKeysWithOwner` rotates to a fresh set of session keys.
- Save `keys` and `proof` immediately; if lost, you must rotate again and re-submit the new values on-chain.
- There is no RPC to generate ownership proofs for existing session keys already in the keystore.
- Manual external key generation/insertion workflows that do not produce a proof are not sufficient for setting keys on-chain.
The private keys are stored in a keystore on your operator server in the `//chains//keystore/` directory. The filenames of these keys are the public key portion of the respective session key, and the file contents represent the private key.
The `keys` output includes five concatenated public keys in this order: `GRANDPA`, `BABE`, `I'm Online`, `Authority Discovery`, `BEEFY`. Prior to the v8 runtime update of Polymesh only 4 keys were generated, excluding the BEEFY key. The keystore will contain a file for each of these keys.
Keystore filenames are prefixed with 4-byte key type identifiers (hex-encoded ASCII), followed by the public key:
- `gran` = `6772616e` (GRANDPA)
- `babe` = `62616265` (BABE)
- `imon` = `696d6f6e` (I'm Online)
- `audi` = `61756469` (Authority Discovery)
- `beef` = `62656566` (BEEFY)
Example filenames:
```plaintext
617564692ea2449927f86a7d67bb6d83f3052201540b3fdb05d4590e3f14ca8088ffd61b
62616265c6cb0c45331ff2c69dfed5fe58d213e1783f9102f336eeb78eae7c8b1b583339
62656566033be800f15a7ee3bd599d25ad1e3cb1f690cbdc27c3f5e1a68784db804cfefe26
6772616e4c28da7efceb18a51eeb605cd42d9d8b0781e25d33215e89837e6a30fdcca3d0
696d6f6ea0efe6d5be790ad3865df836cfc432908e418e6daa0d1d5fb5c0d27535be1930
```
**Wait until your operator node is fully synced with the chain and production-ready before activation.**
### Generating Session Keys in Containerized Polymesh Nodes
Our official container images include a utility to rotate session keys without requiring additional tools like `curl` or exposing unsafe RPC methods outside the container. This utility is located at `/usr/local/bin/rotate`.
In v8, this workflow requires the owner account ID in hex and returns both `keys` and `proof` (equivalent to `author_rotateKeysWithOwner`).
To run the utility inside your container:
```bash
docker exec /usr/local/bin/rotate
```
See our [guide to running a node with Docker](/node/docker#generating-node-session-keys) for more details.
### Getting the Identity of a Node
In v8+, node network keys are not automatically generated when missing. You should provision a persistent node key (for example via `--node-key-file`) and back it up. If you intentionally create a new key with `--unsafe-force-node-key-generation`, use it only for initial bootstrap and remove the flag afterward.
As long as the same node key is preserved, your node peer ID will remain stable across restarts.
There are three ways to obtain the public identity of a node:
- From the operator node logs
- Via an RPC call
- Via the `polymesh key inspect-node-key` command
#### From the Operator Node Logs
Start the node process and look for a line containing `Local node identity`:
```plaintext
2024-11-15 18:50:11 Reserved nodes: []
2024-11-15 18:50:11 Polymesh Node
2024-11-15 18:50:11 ✌️ version 7.0.0
2024-11-15 18:50:11 ❤️ by PolymeshAssociation, 2017-2024
2024-11-15 18:50:11 📋 Chain specification: Polymesh Testnet
2024-11-15 18:50:11 🏷 Node name: woebegone-galley-5149
2024-11-15 18:50:11 👤 Role: FULL
2024-11-15 18:50:11 💾 Database: RocksDb at /var/lib/polymesh/chains/testnet/db/full
2024-11-15 18:50:11 ⛓ Native runtime: polymesh_testnet-7000005 (polymesh_testnet-0.tx7.au1)
2024-11-15 18:50:12 🔨 Initializing Genesis block/state (state: 0xcba3…bce0, header-hash: 0x2ace…d0d6)
2024-11-15 18:50:12 👴 Loading GRANDPA authority set from genesis on what appears to be first startup.
2024-11-15 18:50:13 👶 Creating empty BABE epoch changes on what appears to be first startup.
2024-11-15 18:50:13 🏷 Local node identity is: 12D3KooWSDAHjBmA6j2GyBZPktEz2gLZmtJAc2bWnDV7eCCcgcbC
2024-11-15 18:50:13 💻 Operating system: linux
...
```
In this example, the node's identity is `12D3KooWSDAHjBmA6j2GyBZPktEz2gLZmtJAc2bWnDV7eCCcgcbC`. Save this value, then terminate the process.
#### Via RPC Call
Call the `system_localPeerId` method and read the `result` value:
```bash
curl -s -H "Content-Type: application/json" -d '{"id":1, "jsonrpc":"2.0", "method": "system_localPeerId", "params":[]}' http://localhost:9944 | jq -r .result
```
### Metrics and Monitoring
The recommended method for obtaining metrics is through the node's built-in Prometheus exporter. By default, it binds to `localhost` on port `9615`. Use the `--prometheus-external` flag to expose the exporter port for network-based scraping, or deploy a local agent (e.g., `telegraf`, `grafana-cloud-agent`, or `victoria-metrics-agent`) to collect metrics.
#### Polymesh-Specific Metrics
The basic health of a node can be assessed by monitoring the following metrics:
| Metric | Purpose | Range | Notes |
| ------------------------------------------- | --------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `polymesh_block_height{status="finalized"}` | Finalized block number | ±3 from network average | Verify against the network block number using an external source. |
| `polymesh_block_height{status="best"}` | Best block time | 6s ±2s | Ideal mean block time is 6 seconds; minor variations are normal. |
| `polymesh_ready_transactions_number` | Transactions in ready queue | 0–150 | A growing queue may indicate node latency issues. |
| `polymesh_sub_libp2p_peers_count` | Number of peers | > Minimum peer count | Nodes should maintain connectivity with other operator nodes, ideally with a maximum of three hops to any other operator. |
Guides for monitoring other Substrate-based chains, such as [Polkadot](https://wiki.polkadot.network/docs/maintain-guides-how-to-monitor-your-node), can be referenced for additional approaches to node monitoring.
#### General Node Metrics
In addition to Polymesh-specific metrics, you should monitor basic node health metrics available from generic node exporters or monitoring agents:
| Metric | Range | Notes |
| -------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Free disk space | 30 GB+ or >20% capacity | Ensure sufficient free space for the node to function properly. |
| Free RAM | 1 GB+ | Spikes are acceptable, but an average of 1 GB of free RAM should remain available. |
| CPU usage | 5–50% (overall) | If usage consistently exceeds 50%, consider increasing CPU resources. |
| Network connectivity | >0.1 Mbps bandwidth | Maintain stable connectivity. If under attack (e.g., DDoS), deploy new nodes or restrict connectivity to trusted peers. |
### Bonding POLYX
**Ensure your Polymesh nodes are fully synced and healthy before proceeding. Failure to do so may result in operator penalties.**
To become an operator on Polymesh, you must bond (lock) some POLYX. The account holding the bonded funds is called the **stash account**. For new bonds, the stash account also manages these funds directly — there is no separate controller account to set up (see [Stash Key](#stash-key)).
> **Recommendation:** Ensure your stash account has enough POLYX to cover both the bonded amount and ongoing transaction fees for controller-type actions (setting session keys, unbonding, chilling, etc.), since it now performs both roles.
**Note:** The steps in the following sections can also be completed in a single batched transaction by selecting the **[+ Validator]** button. The instructions below focus on each individual step.
#### Steps to Bond Funds
1. Navigate to the [Staking section](https://mainnet-app.polymesh.network/#/staking/actions).
2. Click **Account Actions**.
3. Select the **[+ Stash]** button.

4. **Complete the following fields:**
- **Payment destination**: Specify where validation rewards should be sent.
5. Click **Bond** and sign the transaction using your stash account. After a few seconds, an `ExtrinsicSuccess` message should appear. Refresh the page if necessary to view your new bonded account details.

### Setting Session Keys
Session keys link your operator node to your stash account (or legacy controller account, if one is in use). If you switch operator nodes, you can update these keys to reflect the new setup, which will take effect in the next session.
Use the session keys generated during your operator node setup.
#### Steps to Set Session Keys
1. Go to the [Staking section](https://mainnet-app.polymesh.network/#/staking/actions).
2. Click **Account Actions**.
3. Locate your bonded account and click **Session Key**.
4. Paste the `keys` value from the `author_rotateKeysWithOwner` result ([generated earlier](#generating-session-keys-with-access-to-the-nodes-rpc-port)) and provide the corresponding `proof`.
5. Click **Set Session Key** and submit the extrinsic.

Once the transaction succeeds, your session keys will be updated, and you're ready to proceed with validation.
### Activating Your Operator Node
Before activation, ensure your setup is verified via telemetry. Improper configurations may result in penalties.
#### Steps to Activate
1. In the UI, click **Validate**.

2. Enter your reward commission percentage, then click **Validate**. At the time of writing, the maximum allowable commission is capped at 10%.

**Congratulations!** Your operator has been added to the queue of available operators. It will be eligible for election in the next operator cycle and will become active at the start of the subsequent era. On Mainnet, this process may take up to 29 hours from setting your validator preferences.
### Stopping Operator Activity
To stop operating as a validator:
1. Navigate to [Staking > Account Actions](https://mainnet-app.polymesh.network/#/staking/actions).
2. Locate your bonded account and click **Stop**.
3. Sign the transaction to confirm the action.
This action removes your node from the list of operators available for election. Your node will remain active for the current era but will become inactive at the start of the next era following its last election.
**Important:** Only stop your node client **after** it is no longer active in the current era to avoid penalties.
#### Reward Destination Considerations
If your reward destination is set to your **Stash account (increase the amount at stake)**:
- Wait until the last reward payment has been received **before** starting the unbonding process.
- Alternatively, change the reward destination to **Stash account (do not increase the amount at stake)** before unbonding to ensure the final reward payment is not bonded.
By doing so, you can ensure the smooth receipt of all pending rewards without interruptions.
---
## Running a Node with Docker
# Running a Polymesh Node with Docker
## Introduction
Docker is an open source containerization platform which allows developers to package, ship, and run any application as a lightweight, portable, self-sufficient container.
For convenience Polymesh offers precompiled Docker container images of Polymesh. These images allow a user to quickly deploy the Polymesh software and its dependencies. A prebuilt container image can be fetched from the [Polymesh Docker Hub repository](https://hub.docker.com/r/polymeshassociation/polymesh/tags). Polymesh also published a sample [Docker-Compose file](https://github.com/PolymeshAssociation/polymesh-tools/tree/main/docker-compose) which can be used to quickly deploy a Polymesh node as part of a multi container environment. The use of Docker Compose is not covered in this guide.
---
**_Following examples are run on the Polymesh testnet chain without SSL. They can be used to quick start and learn how Polymesh can be configured. Please find out how to secure your node, if you want to operate it on the internet. Do not expose RPC endpoints unless they are correctly configured._**
---
This guide was written based on Ubuntu 22.04, but the instructions should be similar for other platforms and assumes Docker is already installed. If Docker is not yet installed follow the official [docker installation instructions](https://docs.docker.com/engine/install/ubuntu/) to install. This guide assumes you are logged in as a non-root user with sudo privileges. For security it is recommended root user login is disabled.
For additional information related to docker commands refer to [Docker Docs](https://docs.docker.com/engine/reference/run/).
## Getting the Polymesh Image
Images are available for mainnet, testnet, staging, and develop chains. There are two flavours available for each: debian and distroless. The distroless version has no shell and thus provides a reduced attack surface, whereas the debian versions shell can help with debugging during the initial setup.
For mainnet, testnet, and staging the images are tagged `--` or `latest--`. At the time of writing, the current published release families include:
- `8.0.1-mainnet-{debian|distroless}` and `latest-mainnet-{debian|distroless}`
- `8.0.1-testnet-{debian|distroless}` and `latest-testnet-{debian|distroless}`
- `8.0.1-staging-{debian|distroless}` and `latest-staging-{debian|distroless}`
Develop chain images are also published from the [`develop` branch](https://github.com/PolymeshAssociation/Polymesh/tree/develop) of the Polymesh repository and are tagged `-develop-` or `latest-develop-`. These are intended for development and testing purposes only and are **not recommended for general use or production deployments**.
The version, chain, and flavour are required for all chains. Use an explicit version tag such as `8.0.1--` for deterministic deployments, or use rolling tags in the form `latest--`.
It is recommended to use a specific `version` number for deterministic versioning. If using rolling `latest--` tags, you may wish to set your image pull policy to `--pull=always` to ensure the latest image is always pulled from the repository when starting the container.
Refer to the [Polymesh Docker Hub repository](https://hub.docker.com/r/polymeshassociation/polymesh/tags) for the available release tags. Your desired release tag can be pulled with the command:
```bash
sudo docker pull polymeshassociation/polymesh:8.0.1-testnet-distroless
```
The tag `:8.0.1-testnet-distroless` should be replaced with the desired release tag.
If you prefer the rolling channel tags, pull for example:
```bash
sudo docker pull polymeshassociation/polymesh:latest-testnet-distroless
```
Polymesh does not publish a plain `:latest` tag. If a release tag is omitted the docker pull command will error.
By default, if the Polymesh container image has not previously been pulled from the repository, it will be pulled when first attempting to run it. You can verify the version installed by running the following command:
```bash
sudo docker run --rm polymeshassociation/polymesh:8.0.1-testnet-distroless --version
```
**All available Polymesh [FLAGS], [OPTIONS] and \ commands can be viewed by calling:**
```bash
sudo docker run --rm polymeshassociation/polymesh:8.0.1-testnet-distroless --help
```
## Running a node
With the image downloaded you can now run your first testnet node:
```bash
sudo docker run -u root --rm -it polymeshassociation/polymesh:8.0.1-testnet-distroless --chain testnet
```
Note: the above command is required to be run as the root user with the flag `-u` or `--user root` in order for the user to have suitable permissions to successfully run the node.
`--rm` is used to automatically remove the container when it exits.
`-it` is used to allocate an interactive, tty virtual terminal session for the container process. This allows node logs to be displayed. When you are satisfied with the configuration of your docker run command and node you can replace `-it` with `-d` or `--detach` to start a container in detached mode so it runs in the background.
`--chain testnet` is used to run a node on the `testnet` blockchain. Replacing `testnet` with `mainnet` would result in a node being created for the Polymesh mainnet blockchain. It can also be one of the predefined ones (`dev`, `local`, or `staging`)
To stop the running container first open a new terminal window. Use `sudo docker ps` to identify the Docker container ID or name for the running Polymesh container. The container is then stopped with the command:
```bash
sudo docker stop
```
Alternatively, Ctrl+C will stop the running container.
## Creating a Volume for Persistent Storage
When the container was stopped all records of it are automatically removed. This is not what you want for a live blockchain node. If you want chain information to persist you need to define a local volume on the operating system that the container will write to. In this example we will use `/var/lib/polymesh/`:
```bash
sudo mkdir /var/lib/polymesh/
```
```bash
sudo docker run \
-u root \
--rm -it \
--volume=/var/lib/polymesh/:/var/lib/polymesh/ \
polymeshassociation/polymesh:8.0.1-testnet-distroless \
--chain testnet \
--base-path /var/lib/polymesh/
```
`--base-path` Specifies a directory where the Polymesh container should store all the data related to this chain. (If it is omitted the default base path is `/.local/share/polymesh/` and the container volume address should be updated to bind to this location)
`-v` or `--volume=` is used to bind mount the volume.
Now when stopping the container and starting a new one the blockchain information persists and the node will recommence syncing the chain where it previously stopped.
## Managing the Node Network Key
In v8+, node network keys are not automatically generated when missing. This is intentional so operators can explicitly manage and back up the key material used for node identity.
Recommended approach:
- Keep your node key in persistent storage and include it in your backup procedures.
- Default node key type is `ed25519` (`--node-key-type ed25519`).
- In the default configuration, the key is stored under:
- `//chains//network/secret_ed25519`
- If your key is in this default location, you do not need `--node-key-file`.
- Use `--node-key-file` only when intentionally storing the key in a custom location.
Preferred one-time key generation:
```bash
sudo docker run \
--user root \
--rm -it \
--volume=/var/lib/polymesh/:/var/lib/polymesh/ \
polymeshassociation/polymesh:8.0.1-testnet-distroless \
key generate-node-key --base-path /var/lib/polymesh/ --chain testnet
```
Optional: inspect the generated key and print its peer ID:
```bash
sudo docker run \
--user root \
--rm -it \
--volume=/var/lib/polymesh/:/var/lib/polymesh/ \
polymeshassociation/polymesh:8.0.1-testnet-distroless \
key inspect-node-key --file /var/lib/polymesh/chains/testnet/network/secret_ed25519
```
If you use the default location shown above, no explicit node-key argument is required at startup. Use an explicit option only for custom layouts:
```bash
--node-key-file /secure/node-keys/validator-1-secret_ed25519
```
For testing you can avoid separate key generation by using `--unsafe-force-node-key-generation`. This forces key generation if the node-key-file does not exist on container start, which is useful for testing but not recommended for production.
`--unsafe-force-node-key-generation` is considered unsafe for active authorities because identity changes can break connectivity with peers that expect a stable peer ID.
```bash
sudo docker run \
--user root \
--rm -it \
--volume=/var/lib/polymesh/:/var/lib/polymesh/ \
polymeshassociation/polymesh:8.0.1-testnet-distroless \
--chain testnet \
--base-path /var/lib/polymesh/ \
--unsafe-force-node-key-generation
```
If you use this flag, remove it for all subsequent restarts.
## Running a Node as a Non Root User
So far your nodes have been run as the root user. For security it is advised to run the container as a non-root user. This section will cover the steps required to run a container as a non-root user. In this section you will create a system user, with no home directory and no login capabilities called `polymesh` and run the Polymesh container as that user.
```bash
sudo useradd -r -M -s /usr/sbin/nologin polymesh
```
Before you run your node you need to ensure this new user has ownership of the storage directory and all files within. This step is essential or the node will fail to run due to permission errors.
```bash
sudo chown polymesh:polymesh /var/lib/polymesh/ -R
```
Now that your system user has ownership of your storage volume you can run the node. To do this you will use the `-u` or `--user` Docker flag. As the “polymesh” user does not exist in the container we must instead specify the UID (user ID) and GID (group ID) for this user explicitly. In the below example `$(id -u polymesh)` and `$(id -g polymesh)` return the UID and GID for the polymesh user. Alternatively, they could be specified explicitly e.g. `--user 999:999` if those are the UID and GID assigned to the polymesh user:
```bash
sudo docker run \
--user $(id -u polymesh):$(id -g polymesh) \
--rm -it \
--volume=/var/lib/polymesh/:/var/lib/polymesh/ \
polymeshassociation/polymesh:8.0.1-testnet-distroless \
--chain testnet \
--base-path /var/lib/polymesh/
```
## Naming your Node and Container
By default, Polymesh assigns a random node name. This can be seen in the node log upon startup. e.g. `Node name: elastic-scarecrow-5175`. Similarly, Docker assigns a random name to the container running the node. For ease of identification users can optionally assign a custom name of their choice for both, using the `--name ` flag as shown in this example:
```bash
sudo docker run \
--name my-container-name \
--user $(id -u polymesh):$(id -g polymesh) \
--rm -it \
--volume=/var/lib/polymesh/:/var/lib/polymesh/ \
polymeshassociation/polymesh:8.0.1-testnet-distroless \
--chain testnet \
--name my-node-name \
--base-path /var/lib/polymesh/
```
The above command runs a Docker container called `my-container-name` and a Polymesh node called `my-node-name`. The name can be arbitrary, but there is a character limit.
Note - the name assigned for the node will be publicly visible in the telemetry sent to Polymesh's servers. (Telemetry is enabled by default). Once your node is running it should appear in the chain telemetry which can be found at the [Polymesh Telemetry Page](https://stats.polymesh.network/).
The Docker container can now be addressed by name. For example, to stop the node you can run the command:
```bash
sudo docker stop my-container-name
```
## Automatically Restarting your Node
Your node should automatically restart in the case of an intermittent failure. This may be a server restart or crash of the Docker container. To achieve this, you will use the `--restart` flag. The recommended restart policy is `always`:
```bash
sudo docker run \
--name my-container-name \
--user $(id -u polymesh):$(id -g polymesh) \
--restart always \
-it \
--volume=/var/lib/polymesh/:/var/lib/polymesh/ \
polymeshassociation/polymesh:8.0.1-testnet-distroless \
--chain testnet \
--name my-node-name \
--base-path /var/lib/polymesh/
```
In the above example we have replaced the `--rm` flag with `--restart always`. To confirm the restart policy is correctly configured you can reboot your server:
```bash
sudo reboot
```
While the server is rebooting your node will no longer be displayed on the Polymesh telemetry page, after it has restarted it will reappear. Following restart, you can also run:
```bash
sudo docker ps
```
to confirm your container is listed as running.
On Debian and Ubuntu, the Docker service is configured to start on boot by default. If the container does not restart automatically you may need to review your configuration for both `docker.service` and `containerd.service`
If you wish to stop your node you can use the command:
```bash
sudo docker stop my-container-name
```
Note: as the restart flag is configured as `always` should the server be rebooted the node will restart. If you no longer want your node to restart, you need to either remove the container or update the container restart policy.
To remove the container:
```bash
sudo docker rm my-container-name
```
To update the container restart policy:
```bash
sudo docker update --restart=no my-container-name
```
## Exposing Container Ports (Libp2p, RPC [HTTP + WebSocket] & Prometheus)
There are 3 main ports you may wish to expose for your node, depending on your application. You should only expose ports you require.
:::note
JSON-RPC HTTP and WebSocket connections are served from the same RPC endpoint. If you are running a pre-v8 release that still has separate HTTP and WS options, continue to use that release's CLI help output.
:::
1. Libp2p – The port that your node will listen for p2p traffic on. Default port: 30333. The default libp2p port can be overridden using the `--port ` flag on your Polymesh instance. This should always be exposed for both your container and your firewall to ensure your node is able to sync with the chain.
2. Remote Procedure Calls (HTTP + WebSocket) – The port that your node will listen for incoming RPC traffic on. Default port: 9944. A custom RPC server TCP port can be specified using the `--rpc-port ` flag on your Polymesh instance.
3. Prometheus – By default Polymesh exposes an endpoint which serves metrics in the [Prometheus exposition format](https://prometheus.io/docs/concepts/data_model/). Default port: 9615. You can change the port with `--prometheus-port `. Prometheus metrics can be disabled with the flag `--no-prometheus`
### Bridged Network
By default, a Docker container creates a separate network stack which is bridged to the host network (`--network=bridge`). An IP address will be allocated for containers on the bridge's network and traffic will be routed through this bridge to the container. To communicate with the container network the required ports must be exposed. Ports are exposed using the `-p` or `--publish` flag in the format `:`. It is best practice to only expose required ports. E.g. add `-p 30333:30333` to the Docker run command to expose the libp2p port.
The default configuration for RPC and Prometheus is to allow traffic from localhost only. As, by default, Docker creates a separate network from the host network, connections from localhost will be refused even if the required ports are published. To allow the host system to communicate with the containerized Polymesh instance, the `--rpc-external` or `--prometheus-external` flags must be added as applicable. Additionally, for RPC connections the `--rpc-cors ` flag should be added, where `` is a comma separated list of origins (protocol://domain e.g. https://mainnet-app.polymesh.network/) allowed to access the RPC server (including WebSocket connections on the same endpoint). A value of `all` will disable origin validation. When running in --dev mode the default is to allow all origins.
Note: adding these flags allows access from outside your host network. Ensure your firewall is configured to allow or block traffic on these ports as required. If running an [operator node](#running-an-operator-node) it is not recommended to expose RPC externally.
Example of node configuration with exposed container ports to allow access from host localhost or WAN:
```bash
sudo docker run \
--name my-container-name \
--user $(id -u polymesh):$(id -g polymesh) \
--rm \
-it \
--volume=/var/lib/polymesh/:/var/lib/polymesh/ \
-p 30333:30333 \
-p 9944:9944 \
-p 9615:9615 \
polymeshassociation/polymesh:8.0.1-testnet-distroless \
--chain testnet \
--name my-node-name \
--base-path /var/lib/polymesh/ \
--rpc-external \
--rpc-cors all \
--prometheus-external
```
### Host Network
An alternate option is to use the host's network stack inside the container. With the `--network` flag set to `host` a container will share the host's network stack and all interfaces from the host will be available to the container. Compared to the default bridge mode, the host mode gives significantly better networking performance since it uses the host's native networking stack whereas the bridge has to go through one level of virtualization through the Docker daemon.
Warning: `--network=host` gives the container full access to local system services and is therefore considered insecure. This option should be used with caution.
Example of Docker node configuration with --network=host:
```bash
sudo docker run \
--name my-container-name \
--user $(id -u polymesh):$(id -g polymesh) \
--rm \
-it \
--volume=/var/lib/polymesh/:/var/lib/polymesh/ \
--network=host \
polymeshassociation/polymesh:8.0.1-testnet-distroless \
--chain testnet \
--name my-node-name \
--base-path /var/lib/polymesh/
```
A benefit of this option is the node can be addressed directly from localhost on the host environment without exposing RPC externally. For example, calling:
```bash
curl -H "Content-Type: application/json" -d '{"id":1, "jsonrpc":"2.0", "method": "system_localPeerId"}' http://localhost:9944/
```
will now return the Peer ID of the node.
---
Note: Not all RPC methods are safe to be exposed publicly. The default RPC methods exposed depends on the node configuration. In both of the previous two examples we did not explicitly specify the RPC methods to expose, therefore it defaulted to `--rpc-methods Auto`. `Auto`: Acts as `Safe` if RPC is served externally, e.g. when `--rpc-external` is passed, otherwise acts as `Unsafe`. `Unsafe`: Exposes every RPC method. `Safe`: Exposes only a safe subset of RPC methods, denying unsafe RPC methods. Disabled RPC methods include `author_hasKey`, `author_hasSessionKeys`, `author_insertKey`, `author_removeExtrinsic`, `author_rotateKeys`, `author_rotateKeysWithOwner`, `babe_epochAuthorship`, `childstate_getStorageSize`, `offchain_localStorageGet`, `offchain_localStorageSet`, `state_getPairs`, `state_queryStorage`, `state_queryStorageAt`, `sync_state_genSyncSpec`, `system_addLogFilter`, `system_dryRun`, `system_dryRunAt` and `system_resetLogFilter`.
The two previous node configuration examples result in different RPC methods being exposed. In the second example with `--network=host` it is possible to call unsafe RPC methods such as `author_rotateKeysWithOwner`, which is not possible for first example with `--rpc-external` configured. External unsafe methods can also be forced by setting `--rpc-methods Unsafe` and `--unsafe-rpc-external` as required.
## Configuring Pruning
In v8+, pruning is configured separately using `--state-pruning` and `--blocks-pruning`.
- `--state-pruning` controls how much historical state data is retained. The default is `256`, which keeps the last 256 finalized blocks.
- `--blocks-pruning` controls how much historical block body data is retained. The default is `archive-canonical`, which keeps finalized blocks. Retaining finalized block bodies allows the finalized chain to be replayed later, but historical state is not directly queryable unless it is also retained with state pruning or reconstructed separately.
Both flags accept `archive`, `archive-canonical`, or a block count (for example `--state-pruning 30000`).
:::tip
Pruning mode is stored in the database when it is first created. On subsequent runs, you can omit the pruning flags and let the node load the stored settings. If you do pass pruning flags again, they must match the stored mode or the node will error. The exception is `--state-pruning NUMBER`: the numeric limit can be changed between runs without purging, but increasing it only affects future pruning and does not restore state that was already pruned. If you intentionally change to a different pruning mode later, the database must be [purged](#purging-chain-database) before restarting with the new settings.
:::
## Purging Chain Database
On occasions you may wish to purge your blockchain database. For example, you may wish to remove partial files created as a result of stopping and starting your node, or if you are changing your node to an operator node and need to change state/block pruning modes. In that instance you will need to either resync from scratch or restore a database backup from a node with the same pruning settings. However, before resyncing the chain you will first need to stop your running node, remove the stopped docker container and purge existing database. (refer to stop and remove commands in the [Automatically Restarting your Node](#automatically-restarting-your-node) section)
Once your container is stopped and removed (if you are not restarting with the same configuration) the database can be purged with the following command:
```bash
sudo docker run \
-u $(id -u polymesh):$(id -g polymesh) \
--rm -it \
-v /var/lib/polymesh/:/var/lib/polymesh/ \
polymeshassociation/polymesh:8.0.1-testnet-distroless \
purge-chain \
--chain testnet \
--base-path /var/lib/polymesh/
```
You will be presented with a prompt to confirm you wish to remove the database which you need to confirm.
```bash
Are you sure to remove "/var/lib/polymesh/chains/testnet/db"? [y/N]: y
"/var/lib/polymesh/chains/testnet/db" removed.
```
Ensure your command specifies the correct `--base-path`, volumes and `--chain` for the database you wish to remove.
## Running an Operator Node
The Operator role on Polymesh is permissioned. Operators must be regulated capital market participants that meet specific criteria and be approved by Polymesh Governance. For more information on becoming an operator visit the [Polymesh Website](https://polymesh.network/node-operators)
Validator nodes use the same pruning defaults described in the [Configuring Pruning](#configuring-pruning) section unless you explicitly override them.
For most validator nodes the defaults are appropriate: `archive-canonical` block pruning retains all finalised block bodies, while 256-block state pruning is sufficient for consensus participation without the significant additional disk space that full state archive requires.
To run a node as an operator, add the `--validator` flag to the node configuration. Below is an example of a node configured as an operator:
```bash
sudo docker run \
--name my-container-name \
--user $(id -u polymesh):$(id -g polymesh) \
--restart always \
-it \
--volume=/var/lib/polymesh/:/var/lib/polymesh/ \
-p 30333:30333 \
-p 9615:9615 \
--ulimit nofile=1024:10240 \
polymeshassociation/polymesh:8.0.1-testnet-distroless \
--chain testnet \
--validator \
--name my-node-name \
--base-path /var/lib/polymesh/ \
--wasm-execution compiled \
--db-cache 4096 \
--db-max-total-wal-size 1024 \
--prometheus-external
```
If you need different retention for validator infrastructure, add the appropriate pruning flags to the command when the database is first created.
## Generating Node Session Keys
Session keys are the keys that an operator node uses to sign data needed for consensus. Session keys are typically generated in the client, although they don't have to be, and are stored on the operator node itself. Session keys are not meant to control funds and should only be used for their intended purpose. They can be used to perform actions that will result in a penalty, like double signing. Hence, it is important to keep these keys secure. They can be changed regularly.
Polymesh uses five session keys:
- GRANDPA: ed25519
- BABE: sr25519
- I'm Online: sr25519
- Authority Discovery: sr25519
- BEEFY: ecdsa (Polymesh V8+ only)
These keys can either be generated offline and injected in the operator node or can be generated within the operator node by calling the appropriate RPC method. Once generated the session keys should be persisted.
The official Polymesh docker images contain a small binary to generate the session keys and insert them in the keystore. This is the simplest method to generate session keys as it does not require exposing unsafe RPC methods externally or installing `curl` in the container or in a sidecar.
This binary is located in `/usr/local/bin/rotate`. In v8, session key rotation requires an ownership proof, so the rotate workflow uses `author_rotateKeysWithOwner` and requires your **stash AccountId** in hex as input (`0x` + 32-byte raw public key bytes). An account transformation tool such as https://polymesh.subscan.io/tools/format_transform can be used to convert your stash account address to the required hex public key format.
```bash
sudo docker exec -u $(id -u polymesh):$(id -g polymesh) my-container-name /usr/local/bin/rotate
```
Example output:
```json
{
"keys": "0x4e6106...b0546b",
"proof": "0x59e7e5...85901"
}
```
If needed, you can call the RPC endpoint directly:
```bash
curl -X POST http://127.0.0.1:9944 -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"author_rotateKeysWithOwner","params":[""],"id":1}'
```
You must retain both values from the response:
- `keys`: The concatenated public session keys.
- `proof`: The ownership proof required when setting session keys on-chain.
Important behavior changes:
- Each call to `author_rotateKeysWithOwner` rotates to a fresh set of session keys.
- Save `keys` and `proof` immediately; if lost, you must rotate again and re-submit the new values on-chain.
- There is currently no RPC to generate ownership proofs for existing session keys already in the keystore.
- Manual external key generation/insertion workflows that do not produce a proof are not sufficient for setting keys on-chain.
The private keys are stored in a keystore on your operator server, for example in `//chains/testnet/keystore/`.
After you have generated your session keys refer to the [Polymesh Operator Guide](https://github.com/PolymeshAssociation/polymesh-tools/tree/main/docs/operator#bonding-polyx) for steps required to bond POLYX, set session keys and activate your operator node. **Ensure you wait before activating your operator node until all your nodes are fully synced with the chain.**
---
## Smart Contracts
## Overview
Polymesh includes rich native functionality for identity, assets, compliance, portfolio management, and settlement.
Smart contracts are used to add custom workflow and business logic on top of those native capabilities.
This allows teams to keep core regulated operations on the native layer while implementing bespoke behavior in contracts, such as:
- exchange and routing logic
- custody workflow automation
- permissioned operations with custom approval paths
- upgradeable application-level policy logic
## `pallet-revive`: a dual-VM contract engine (PolkaVM + EVM)
The smart contract pallet on Polymesh is `pallet-revive`. It is a **dual-VM contract execution engine**: it runs two different virtual machines, and which one executes a given contract is determined by how that contract was compiled — not by which way you call it.
- **PolkaVM** (a RISC-V-based VM) executes **PolkaVM bytecode**. Solidity compiled with the [`resolc`](https://github.com/paritytech/revive) compiler (instead of `solc`) targets PolkaVM. This path does **not** provide full EVM compatibility — some EVM semantics and opcodes are unsupported or behave differently, so a contract that depends on exact EVM behaviour should not assume it carries over.
- **revm** (a Rust implementation of the Ethereum Virtual Machine) executes standard **EVM bytecode**. Solidity compiled with the ordinary `solc` compiler produces EVM bytecode that deploys and runs in `revm` with **full EVM compatibility**. This is the path to use when a contract must behave exactly as it would on Ethereum.
In short: **`resolc` → PolkaVM** (native execution, partial EVM compatibility) and **`solc` → EVM bytecode → `revm`** (full EVM compatibility). EVM-bytecode execution is a distinct execution path enabled by the `AllowEVMBytecode` runtime flag (see [Runtime configuration](#runtime-configuration)) — not merely an interface layered on top of PolkaVM. Both VMs live behind the same `pallet-revive` calls, storage, and account model.
:::info The VM is chosen from the uploaded code, not the submission path
When you deploy a contract you upload its compiled code, and the runtime selects the VM from the code itself — PolkaVM if it's RISC-V/PolkaVM bytecode, `revm` if it's EVM bytecode. This is **independent of how you submit the transaction**. A Solidity/EVM-bytecode contract can be deployed and called from an ordinary **Substrate transaction** (e.g. polkadot.js or subxt, via `instantiate_with_code` / `call`) just as well as from standard Ethereum tooling through the [`eth-rpc` proxy](#running-evm-json-rpc). The Ethereum JSON-RPC path is a convenience for standard ETH tooling and workflows — it is not a requirement for deploying or using EVM contracts.
:::
## Address mapping
Polymesh accounts are 32-byte (`AccountId32`), not 20-byte Ethereum addresses. `AddressMapper = pallet_revive::AccountId32Mapper` bridges the two, and the two directions work differently:
- **Native account → Ethereum address**: computed by hashing the 32-byte account with Keccak-256 and taking the last 20 bytes. This is a one-way derivation — the original 32-byte account cannot be recovered from the 20-byte hash output alone.
- **Ethereum address → native account**: if the account has previously called `map_account`, the chain looks up the real account via the stored `OriginalAccount` mapping. If it hasn't, the chain falls back to a deterministic **fallback account**: the 20-byte address padded with twelve `0xEE` bytes to make 32 bytes. `unmap_account` removes a stored mapping.
:::danger Map your account before receiving tokens at its Ethereum-style address
The native-to-Ethereum direction above is a one-way hash, so the chain cannot invert it on its own. Until an account calls `map_account`, any transfer sent to that account's derived Ethereum-style address is credited to the `0xEE`-padded **fallback account** instead — a distinct 32-byte account from the real one.
This is not automatically lost: the real account holder can call `dispatch_as_fallback_account` to dispatch a call (e.g. a transfer) as that fallback account and move the funds out, since the runtime re-derives the same fallback account from their signed origin. But this is a manual recovery step that most wallets and tooling won't surface by default, so **the safe practice is to call `map_account` before ever advertising or receiving funds at your derived Ethereum-style address** — mapped accounts receive transfers directly, with no recovery step ever needed.
:::
Tools such as [Subscan's account-conversion utility](https://polymesh.subscan.io/tools/format_transform) can compute both directions for you (Ethereum → SS58 pads with `0xEE`; SS58 → Ethereum performs the Keccak-256 derivation) — useful for looking up addresses, but it does not perform or substitute for the on-chain `map_account` call.
## Calls
`pallet-revive` exposes the standard upstream call set — Polymesh has not added or removed any calls at the pallet level; customization is entirely in runtime configuration (below):
- `call`, `instantiate`, `instantiate_with_code` — native (Substrate) calls to invoke or deploy a contract. `instantiate_with_code` accepts **either** PolkaVM or EVM bytecode; the runtime selects the VM from the uploaded code, so these deploy Solidity/EVM contracts to `revm` too — not only PolkaVM contracts.
- `eth_transact`, `eth_instantiate_with_code`, `eth_call`, `eth_substrate_call` — Ethereum-transaction-shaped entry points (RLP-encoded, ETH-signed) that the `eth-rpc` proxy uses so standard Ethereum tooling can reach the same contracts. They are an alternative submission path, not a different VM.
- `upload_code`, `remove_code`, `set_code` — manage contract code independently of instances (again, either bytecode kind)
- `map_account`, `unmap_account` — manage the reversible `AccountId32` ↔ Ethereum address mapping
- `dispatch_as_fallback_account` — dispatch a call as a contract's fallback account
## Runtime configuration
Values set in `impl pallet_revive::Config for Runtime`:
| Setting | Value | Notes |
| ------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `ChainId` | `1_641_820` (Mainnet), `1_641_819` (Testnet), `1_641_818` (Develop) | The EVM chain ID wallets and tooling (MetaMask, ethers.js) need to target the right network |
| `NativeToEthRatio` | `10^12` | Bridges Polymesh's 6-decimal POLYX to Ethereum's 18-decimal wei convention |
| `AllowEVMBytecode` | `true` | Enables the `revm` EVM-bytecode path (`solc` output) alongside PolkaVM contracts |
| `GasScale` | `100` | Scales EVM gas to Polymesh's weight-based fee model |
| `Precompiles` | `()` | No custom precompiles configured yet — see [Precompiles](#precompiles) below |
| `AddressMapper` | `AccountId32Mapper` | See [Address mapping](#address-mapping) above |
## Interacting with native Polymesh functionality
### Precompiles
A precompile is a fixed contract address that, instead of running interpreted bytecode, triggers native logic on the runtime side — from a contract's perspective it looks like a normal contract implementing some interface (an ABI, for the EVM case), but calls to it are handled directly by a pallet rather than by executing PolkaVM/EVM code. This is the mechanism `pallet-revive` uses to expose native chain functionality to contracts: `impl pallet_revive::Config for Runtime` has a `Precompiles` associated type (see the [runtime configuration](#runtime-configuration) table above) that lists which precompiles are wired in for a given network.
:::caution No native-asset precompiles yet
As of this writing, Polymesh's `Precompiles` configuration is empty (`()`) on Mainnet, Testnet, and Develop. There is currently **no** supported way for a contract — PolkaVM or EVM — to call into the `Asset`, `Settlement`, or `Identity` pallets. Contracts are limited to their own storage and native POLYX balance flows (receiving, holding, transferring), which don't depend on a precompile.
Precompiles for interacting with native assets (such as an ERC-20-style interface for fungible assets — `transfer`, `approve`, `transferFrom`, `balanceOf`, `allowance`) are planned for a future release. This page will be updated once they ship; until then, treat contract-driven native-asset interaction as a planned capability, not an available one.
:::
### Allowances (`asset::approve` / `settlement::transfer_funds`)
The `Asset` and `Settlement` pallets provide an ERC-20-style `approve`/`transferFrom` pattern at the chain level: an asset holder calls `asset::approve(asset_id, spender, amount)` to authorize a spender to move up to `amount` of the asset on their behalf, without granting the spender a broader identity permission. The spender then calls `settlement::transfer_funds` naming the owner's account as the source — no separate signature from the owner is required for that transfer, and the allowance is drawn down accordingly. See [Allowances](/core/assets/fungible#allowances) and [Direct Transfers](/settlement#direct-transfers-transfer_funds) for the full mechanics. This is a general chain feature usable today by any permitted caller (an off-chain service, another pallet, or — once the precompile above ships — a contract); it does not itself require a contract.
## Running EVM JSON-RPC
`pallet-revive` is a Substrate pallet, not a full Ethereum node — it doesn't speak Ethereum's JSON-RPC (`eth_call`, `eth_sendRawTransaction`, `eth_getBalance`, etc.) directly. A separate proxy process, `pallet-revive-eth-rpc` (binary name `eth-rpc`), translates standard Ethereum JSON-RPC into calls against the node. Run it alongside your node to point MetaMask, ethers.js, or other standard Ethereum tooling at Polymesh, using the `ChainId` for your target network from the table above.
The `eth-rpc` proxy exists purely to support standard Ethereum tooling and workflows. It is **not** required to use EVM contracts: the same `revm`-executed Solidity contracts can be deployed and called directly through ordinary Substrate transactions (`instantiate_with_code` / `call`) using e.g. polkadot.js or subxt. Which VM runs a contract depends on the uploaded bytecode, not on whether you go through `eth-rpc` — see [the note above](#pallet-revive-a-dual-vm-contract-engine-polkavm--evm).
:::info Polkadot's smart contracts documentation
Polymesh's `pallet-revive` is built on the same upstream pallet used across the Polkadot ecosystem, so [Polkadot's smart contracts documentation](https://docs.polkadot.com/smart-contracts/) is a useful reference for standard EVM/Solidity tooling and workflows — dev environments (Remix, Hardhat, Foundry), libraries (ethers.js, viem, web3.js), an ERC-20/NFT/Uniswap cookbook, and general EVM-vs-PVM concepts. Treat it as a guide to the surrounding tooling ecosystem, not to Polymesh's own configuration: runtime settings like `ChainId`, `Precompiles`, and address mapping are specific to Polymesh's deployment and are documented above.
:::
## Error handling
`pallet-revive` has a large, themed error surface (contract/code lookup, execution & gas, calls & reentrancy, storage & deposits, code/blob validation, delegate dependencies, account mapping & syscalls, and EVM/Ethereum compatibility). For a full list of error definitions refer to the chain's own metadata for the network and runtime version you're targeting (via [Subscan runtime explorer](https://polymesh-testnet.subscan.io/runtime/Revive), [polymesh developer app](https://mainnet-app.polymesh.network/), polkadot.js apps, subxt, or the Polymesh SDK's generated types). If you're building a wallet, indexer, or dApp framework against `pallet-revive`, treat the metadata as the source of truth; expect most error conditions to be execution or storage-limit conditions specific to the PolkaVM environment.
## Getting started checklist
1. Target `pallet-revive` for all contract work on Polymesh.
2. To interact with contracts using standard Ethereum tooling, run the `eth-rpc` proxy alongside your node and use the correct `ChainId` for your network.
3. Have users call `map_account` **before** they receive funds at their derived Ethereum-style address — see the warning in [Address mapping](#address-mapping).
4. Contracts cannot yet call into native Polymesh pallets (no precompiles are configured) — see [Precompiles](#precompiles). Native-asset precompiles are planned for a future release.
---
## Polymesh Chain Changelog - v7.4 to v8.0
This is the entry point for the Polymesh **v7.4 → v8.0** runtime changelog. v8.0 is a major release: `spec_version` moves from `7_004_001` to `8_000_010` and `transaction_version` moves from `7` to `8`. A transaction-version bump means clients must not assume any pre-v8 extrinsic encoding, call index, or event shape still holds — every integration that talks to the chain directly (not through the SDK) needs to re-verify its assumptions against this changelog before upgrading.
## Who this is for
- **Exchanges, custodians, wallets** — see [Balances & Transfers](./020-balances-and-transfers.mdx)
- **Validator/node operators** — see [Staking & Validators](./030-staking-and-validators.mdx) for pallet/API changes, and the [Validator Node Guide](/developer-resources/validator-node-guide/) for operational steps
- **Asset issuers, transfer agents, integrators building on Settlement/Portfolio** — see [Native Asset Holdings](./040-native-asset-holdings.mdx) and [Settlement & Instructions](./050-settlement-and-instructions.mdx)
- **Anyone registering identities or managing DID registrars** — see [Identity & Onboarding](./060-identity-and-onboarding.mdx)
- **Anyone subsidizing transaction fees or relaying signed calls** — see [Relayer & Subsidies](./070-relayer-and-subsidies.mdx)
- **Smart contract developers** — see [Smart Contracts](./080-smart-contracts-evm.mdx)
- **Anyone evaluating Confidential Assets** — see [Confidential Assets](./090-confidential-assets.mdx) — not available on Mainnet
- **Wallet developers and anyone integrating hardware wallets or offline signers** — see [Other Runtime Changes](./100-other-runtime-changes.mdx#transaction-extension-checkmetadatahash-merkleized-metadata) and the [Ledger Hardware Wallet](/getting-started/keys/ledger) guide
## Key themes
### Balances pallet replaced with the upstream standard
Polymesh's custom `Balances` pallet (`pallets/balances`) is removed. Chain v8 uses the standard Polkadot SDK `pallet-balances`, extended with a `Memo` type to preserve memo-tagged transfers. Call indices, event shapes, and the `system.account` storage layout all changed. See [Balances & Transfers](./020-balances-and-transfers.mdx) for detail.
### Staking pallet replaced with the upstream standard; permissioning split into a new `Validators` pallet
Polymesh's custom `Staking` pallet (`pallets/staking`) is also removed. Chain v8 uses the standard upstream `pallet-staking`, and the Polymesh-specific permissioned-validator governance (`add_permissioned_validator`, commission caps, slashing-allowed switches) moves to a **new standalone pallet, `Validators`** (`pallet_validators`, runtime index 16), alongside `Staking` (index 17). See [Staking & Validators](./030-staking-and-validators.mdx) for detail.
### Accounts can hold Polymesh assets directly
From v7.4, accounts could already hold assets directly. This was done as a non breaking change by extending the `PortfolioKind` type. A `PortfolioKind::AccountId` variant bolted onto the existing portfolio-kind type. That still went through `PortfolioId`, which always carries a `did: IdentityId` field — so a linked identity was still required alongside the account key, not a DID-free wallet-only model. Chain v8 replaces this with a proper, first-class `AssetHolder`/`AssetHolderKind` abstraction, used consistently across the `Asset`, `Nft`, and `Settlement` pallets — issuance, redemption, controller transfers, and settlement legs/affirmations can now all target an account directly, not only a portfolio. Accounts still need be linked to a DID on chain to hold assets — see [Native Asset Holdings](./040-native-asset-holdings.mdx) (Asset/Nft/Portfolio) and [Settlement & Instructions](./050-settlement-and-instructions.mdx) (legs, affirmations, receipts).
### `pallet-revive` added (a dual-VM PolkaVM + EVM contract engine)
Chain v8 adds `pallet-revive` to the mainnet runtime as its only smart contract pallet. It is a dual-VM engine: `resolc`-compiled Solidity runs as native PolkaVM bytecode (partial EVM compatibility), while `solc`-compiled EVM bytecode runs in a separate Rust EVM (`revm`) for full EVM compatibility — with standard Ethereum JSON-RPC tooling reaching it via a new `eth-rpc` proxy. The legacy Wasm/ink! contracts pallet (`pallet-contracts`) has been completely removed — it is not part of the v8 runtime, and this isn't a breaking removal in practice since no contracts were deployed on Mainnet in the first place. See [Smart Contracts](./080-smart-contracts-evm.mdx) for detail.
### CDD service providers renamed to DID registrars; child identities removed
`CddServiceProviders` is renamed `DidRegistrars`, `Identity` gains permissionless (`self_register_did`) and registrar-gated (`register_did`) ways to obtain a DID without a CDD claim, and child-identity calls/events/storage are removed entirely. The legacy `cdd_register_did`/`cdd_register_did_with_cdd` calls are not removed, but are deprecated and no longer attach a `CustomerDueDiligence` claim — they now behave the same as `register_did`, just with `secondary_keys` support. See [Identity & Onboarding](./060-identity-and-onboarding.mdx) for the pallet-level detail, and the core-concepts [Identity Onboarding](/identity/onboarding) doc for the conceptual model.
### Subsidies get a dedicated approve/accept/revoke flow
The `Relayer` pallet's subsidy grant/accept handshake no longer goes through the generic `Identity` authorization system. `approve_subsidy` (renamed from `set_paying_key`) now writes directly to a `PendingSubsidies` storage, and the user calls the new `accept_subsidy` to accept it — no authorization ID involved. `relay_tx` also moves from `Utility` to `Relayer`, keeping its nonce-based replay protection but gaining a new `expires_at` check on top, and moving the nonce from a caller-supplied argument to server-tracked state. See [Relayer & Subsidies](./070-relayer-and-subsidies.mdx) for detail.
### Merkleized metadata enables generic hardware-wallet signing
Chain v8 adds the `CheckMetadataHash` transaction extension (the "Merkleized metadata" scheme, RFC-0078) to the signed-extension tuple. A signer can now include a hash of the runtime's own metadata in the signed payload, letting a device verify the metadata it uses to render a transaction against what it's actually signing. This is what allows **generic, non-Polymesh-specific signers — including the generic Polkadot Ledger app — to decode and display full Polymesh transaction details**, not just Polymesh-specific applications. Polymesh's own dedicated Ledger app (developed by Zondax) is updated to support this scheme as well. See [Other Runtime Changes](./100-other-runtime-changes.mdx#transaction-extension-checkmetadatahash-merkleized-metadata) for the chain-level detail and transaction-construction impact, and the [Ledger Hardware Wallet](/getting-started/keys/ledger) guide for the wallet-side view (choosing between the Polymesh app and the generic Polkadot app, and the derivation-path implications of each).
### Confidential Assets pallet added (not on Mainnet)
A new `ConfidentialAssets` pallet (Polymesh Confidential Assets / "PCA") lands in v8, implementing privacy-preserving, ZK-proof-based transfers with auditor/mediator compliance controls. It is pre-release, pending a comprehensive code audit, and is **not included in the Mainnet runtime** — it's available on Testnet and Develop only. See [Confidential Assets](./090-confidential-assets.mdx) for the chain-level reference, and the [Confidential Assets](/confidential-assets) docs for the concepts and workflows.
### Framework-level and Miscellaneous other changes
- Runtime pallet composition moved from the legacy `construct_runtime!` macro to the newer `#[frame_support::runtime]` attribute syntax — functionally equivalent, but worth knowing if you generate tooling from the runtime source directly.
- `RandomnessCollectiveFlip` and `Bridge` (already unused) pallets removed.
- New pallets: `Validators` (16), `PolymeshTransactionPayment` (51), `Beefy` (52), `Mmr` (53), `MmrLeaf` (54), plus `MultiBlockMigrations` infrastructure.
- `CddServiceProviders` pallet renamed to `DidRegistrars`.
- Transaction fee handling splits across the standard `TransactionPayment` and the slimmed-down `PolymeshTransactionPayment`; `Sudo` moves to the upstream standard and gains `remove_key`; `System` gains authorized two-step runtime upgrades; `Scheduler` gains retry management. See [Other Runtime Changes](./100-other-runtime-changes.mdx) for detail.
## Migration checklist
1. Regenerate any hand-maintained call-index/event-index tables — do not assume v7 indices carry over, given the transaction-version bump.
2. If you parse `balances.Transfer` for deposits, update to the new 3-field shape (`from`, `to`, `amount`) and confirm your `system.account` balance parsing handles the `frozen`/`flags` layout. See [Balances & Transfers](./020-balances-and-transfers.mdx).
3. If you operate a validator node, review [Staking & Validators](./030-staking-and-validators.mdx) for API changes and the [Validator Node Guide](/developer-resources/validator-node-guide/) for the v8 operational updates (BEEFY session key, node key generation, pruning defaults) before upgrading.
4. If your integration calls `add_permissioned_validator`, `remove_permissioned_validator`, `change_slashing_allowed_for`, or `set_commission_cap`, note these now live on the `Validators` pallet, not `Staking`; and the v7.4 `payout_stakers_by_system` call is removed entirely — see [Staking & Validators](./030-staking-and-validators.mdx).
5. If you hold or transfer Polymesh assets outside of portfolios, or build tooling against `PortfolioId`, review [Native Asset Holdings](./040-native-asset-holdings.mdx) and [Settlement & Instructions](./050-settlement-and-instructions.mdx).
6. Re-check any code that special-cases `RandomnessCollectiveFlip` by name, and rename `CddServiceProviders` references to `DidRegistrars` (see [Identity & Onboarding](./060-identity-and-onboarding.mdx)).
7. If you subsidize accounts or relay signed transactions, review [Relayer & Subsidies](./070-relayer-and-subsidies.mdx) — the accept flow, event shapes, and `relay_tx` all changed.
8. If you deploy or call smart contracts, review [Smart Contracts](./080-smart-contracts-evm.mdx) — target `pallet-revive`.
9. If you're evaluating Confidential Assets, connect to Testnet — it is not on Mainnet, and the Devnet it previously ran on is being retired — and review [Confidential Assets](./090-confidential-assets.mdx) before writing any code against it.
10. Review [Other Runtime Changes](./100-other-runtime-changes.mdx) if you interact with transaction fee queries, `Sudo`, runtime upgrades, or the `Scheduler` directly.
11. If you call Runtime APIs directly via `state_call` (not just extrinsics/storage), review [Runtime APIs & RPC](./120-runtime-apis-and-rpc.mdx) — `IdentityApi`, `StakingApi`, `AssetApi`, and `SettlementApi` all changed. Also check that page if you call `identity_isIdentityHasValidCdd`, `identity_validCDDClaims`, `asset_transferReport`, `nft_transferReport`, `staking_getCurve`, or `settlement_getAffirmationCount` directly by JSON-RPC name — five of those are removed and one changes its parameters.
12. If you construct and sign extrinsics manually, or build/integrate a hardware wallet or offline signer, account for the new `CheckMetadataHash` transaction extension — see [Other Runtime Changes](./100-other-runtime-changes.mdx#transaction-extension-checkmetadatahash-merkleized-metadata).
## Full pallet API reference
For a mechanically generated, literal diff of every call, event, error, storage item, and constant across all pallets (not just the ones covered in the topic pages above), see the [Full Pallet API Reference](./110-pallet-api-reference.mdx). It's derived directly from runtime metadata rather than source review, so it's a good cross-check alongside the narrative pages.
## Runtime APIs & JSON-RPC
If you call Runtime APIs directly (`state_call`) or specific custom JSON-RPC methods by name, see [Runtime APIs & RPC](./120-runtime-apis-and-rpc.mdx) — `IdentityApi`, `StakingApi`, `AssetApi`, and `SettlementApi` all changed shape in v8, and five custom JSON-RPC methods (`identity_isIdentityHasValidCdd`, `identity_validCDDClaims`, `asset_transferReport`, `nft_transferReport`, `staking_getCurve`) are removed.
---
## 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](./010-overview.mdx).
**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. Upstream call that would reap the sender account if it dropped below the existential deposit — but Polymesh keeps account reaping disabled, so in practice this behaves like a plain transfer |
| — | `transfer_keep_alive` (3) | New. Upstream call that would fail if the transfer dropped the sender below the existential deposit. With reaping disabled on Polymesh, it behaves equivalently to `transfer_allow_death` |
| — | `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)` |
| `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.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 (ED = 1 in v8, was 0 in v7.4; reaping stays disabled)
transferable = free - max(ED, frozen - reserved)
```
### 3. `Transfer` event no longer carries identity or memo fields
```
// v7.4
balances.Transfer(from_did: Option, from: AccountId, to_did: Option, to: AccountId, amount: Balance, memo: Option)
// v8.0
balances.Transfer(from: AccountId, to: AccountId, amount: Balance)
```
`balances.TransferWithMemo(from, to, amount, memo: Option)` 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](./030-staking-and-validators.mdx).
`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` 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>` to a bounded `WeakBoundedVec, 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.
### 7. Existential deposit raised from `0` to `1`
In v7.4 the `ExistentialDeposit` was `0`. In v8 it is `1` — one base unit (`0.000001` POLYX), the lowest non-zero value — on all runtimes. It was raised because a zero existential deposit interfered with smart-contract (`pallet-revive`) account creation and termination; a non-zero minimum keeps contract accounts well-defined.
**Account reaping remains disabled on Polymesh.** Despite the non-zero existential deposit, the runtime does not reap accounts whose balance falls to zero, so an emptied account — its `system.account` entry and its nonce — persists as before. In practice `transfer_keep_alive` and `transfer_allow_death` behave equivalently for integrations, exactly as they did under `ED = 0`.
This is worth knowing if you read the `ExistentialDeposit` constant from metadata (it is now `1`, not `0`) or use it in transferable-balance math, but it does not change transfer semantics or the account lifecycle.
## What did _not_ change
- Account reaping stays disabled: accounts are not reaped at a zero balance, so `transfer_keep_alive` and `transfer_allow_death` behave equivalently for exchange integrations — even though the existential deposit moved from `0` to `1` (see [section 7](#7-existential-deposit-raised-from-0-to-1)).
- 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
1. Stop using `transfer` and `set_balance` — they no longer exist. Use `transfer_allow_death`/`transfer_keep_alive`/`transfer_all` and `force_set_balance` respectively.
2. Update call-index tables for `Balances` — indices shifted for every retained call.
3. Update `balances.Transfer` decoding to the 3-field shape; keep `TransferWithMemo` decoding as-is if you already adopted it in v7.4.
4. Update `system.account` decoding for `frozen`/`flags` instead of `misc_frozen`/`fee_frozen`, and switch any transferable-balance calculation to `free - max(0, frozen - reserved)`.
5. Do not rely on `ReceiverCddMissing` as a transfer-failure signal — it no longer exists.
6. If you match errors/events by numeric index rather than name, stop — indices are not stable identifiers across this upgrade.
7. If you read `ExistentialDeposit` from metadata or use it in balance math, note it is now `1` (was `0` in v7.4). Account reaping stays disabled, so transfer and account-lifecycle behaviour is unchanged — see [section 7](#7-existential-deposit-raised-from-0-to-1).
---
## Staking & Validators Changelog - v7.4 to v8.0
This page covers changes to the `Staking` pallet and the new `Validators` pallet. It is part of the [v7.4 → v8.0 changelog](./010-overview.mdx).
**Audience:** anyone calling staking extrinsics directly, running tooling against staking events/storage, or operating a validator node.
For CLI and node-operation changes (session keys, node keys, pruning), see [Node operators](#node-operators) below, which points to the already-updated operational guides.
**Reading staking state (indexers, dashboards, exchanges)? Start here.** Most of this page is organised by chain internals (extrinsics → events → storage → runtime API), but several v8 changes alter what read-only queries **return** without ever throwing an error — deprecated-but-still-present storage, relocated events, and `Option`-ified maps. Because these don't fail loudly, they surface as missing or stale data in your app rather than a failed call. Review these before upgrading:
- **Exposure reads:** `ErasStakers` / `ErasStakersClipped` are no longer written for post-upgrade eras. Reading them for a current era succeeds but returns empty exposure — migrate to `ErasStakersOverview` + `ErasStakersPaged` (paged). See [section 12](#12-exposure-storage-is-now-paged--old-exposure-maps-are-no-longer-populated).
- **Event subscriptions:** the permissioning events (including `InvalidatedNominators`) are now emitted by the `Validators` pallet, not `Staking` — a subscription filtered by `section === 'staking'` will miss them. See [section 1](#1-permissioning-calls-events-and-errors-moved-to-validators).
- **`Payee` reads:** now `OptionQuery` — handle the `None` case (a bond with no payee configured). See [section 12](#12-exposure-storage-is-now-paged--old-exposure-maps-are-no-longer-populated).
- **Nomination limits:** the `MaxNominations` runtime _constant_ is gone — query the limit via the `StakingApi::nominations_quota(balance)` Runtime API instead (effectively 16). Separately, `MaxNominatorRewardedPerValidator` is replaced by `MaxExposurePageSize` (64) with paging. See [section 11](#11-maxnominations-constant-removed--query-the-limit-via-a-runtime-api) and [section 12](#12-exposure-storage-is-now-paged--old-exposure-maps-are-no-longer-populated).
- **RPC:** `staking_getCurve` JSON-RPC is removed; the new `StakingApi` methods (`nominations_quota`, `eras_stakers_page_count`, `pending_rewards`) are `state_call`-only, with no JSON-RPC wrapper. See [Runtime APIs & RPC](./120-runtime-apis-and-rpc.mdx#stakingapi--method-set-fully-replaced).
---
## Overview
In chain v7, `Staking` was a single Polymesh-specific pallet combining standard staking mechanics (bonding, nominating, payouts) with Polymesh's permissioned-validator governance (which identities may run a validator, commission caps, slashing switches). In chain v8, that pallet is split in two:
- **`Staking`** (runtime index 17) — the standard upstream `pallet-staking`.
- **`Validators`** (runtime index 16) — a new Polymesh pallet holding everything permissioning-related.
The two are wired together: `pallet_staking::Config::Permissioned = Validators`, so `Staking` calls into `Validators` to check whether an identity is allowed to validate. `Validators` also owns the reward-curve/inflation configuration (`EraPayout = pallet_validators::PolymeshConvertCurve<...>`).
If your integration calls the permissioning extrinsics below, or depends on the old combined pallet, update it — the calls didn't disappear, but they now live at a different pallet index.
## Breaking changes
### 1. Permissioning calls, events, and errors moved to `Validators`
These calls move from `Staking` to `Validators` (index 16) unchanged in signature:
- `add_permissioned_validator`
- `remove_permissioned_validator`
- `change_slashing_allowed_for`
- `update_permissioned_validator_intended_count`
- `chill_from_governance`
- `set_commission_cap`
The v7.4 `payout_stakers_by_system` call is **not** among them — it is removed entirely in v8, along with the scheduler-driven automatic reward payout it backed. See [section 14](#14-automatic-reward-payout-mechanism).
Along with the events `PermissionedIdentityAdded`, `PermissionedIdentityRemoved`, `InvalidatedNominators`, `SlashingAllowedForChanged`, `RewardPaymentSchedulingInterrupted`, `CommissionCapUpdated`, and errors `StashIdentityDoesNotExist`, `StashIdentityNotPermissioned`, `IdentityIsAlreadyPermissioned`, `IntendedCountIsExceedingConsensusLimit`, `IdentityNotFound`, `ValidatorNotFound`, `CommissionTooHigh`, `CommissionUnchanged`.
These events are now emitted by the `Validators` pallet (index 16), not `Staking`. **Any event subscription filtered by pallet/section name — e.g. `section === 'staking'` — will silently stop seeing them.** Re-point those filters at `Validators`. Note the asymmetry: the extrinsics that a nominator or validator actually signs (`nominate`, `bond`, `validate`, and so on) stay on `Staking`, while these permissioning/administration events moved to `Validators` — so the pallet that emits an event is no longer necessarily the pallet the triggering call lives on.
`Validators` also declares a `Nominated` event, but nothing in v8 actually emits it. The upstream `pallet-staking` doesn't have a nomination event at all, and this Polymesh-specific one (which used to include the nominator's identity) wasn't reintroduced when the pallet split happened. Don't build tooling that waits for it.
One error is renamed as part of this move, reflecting the DID-only terminology shift: `StashIdentityNotCDDed` / `IdentityIsMissingCDD` → `IdentityIsInactive`.
### 2. `bond` no longer takes a controller
```
// v7.4
bond(controller: AccountIdLookup, value: Compact, payee: RewardDestination)
// v8.0
bond(value: Compact, payee: RewardDestination)
```
### 3. `set_controller` no longer takes an argument
```
// v7.4
set_controller(controller: AccountIdLookup)
// v8.0
set_controller()
```
Calling `set_controller` now always resets the controller **to the stash address** — you can no longer point the controller at a different account. It errors with `AlreadyPaired` if the stash is already its own controller. Existing bonds that already have a separate controller keep working after the upgrade; nothing forces them onto the controller-less model. Migration is opt-in and stash-initiated: the stash owner calls `set_controller` (or the admin-only `deprecate_controller_batch`, below) whenever they choose to. `nominate` and other controller-gated calls are still signed by whichever account is currently the controller — for a stash that hasn't migrated, that's still the separate controller account, not the stash.
### 4. `chill_other` takes a stash, not a controller
```
// v7.4
chill_other(controller: AccountId)
// v8.0
chill_other(stash: AccountId)
```
### 5. New calls
| Call | Purpose |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `payout_stakers_by_page(validator_stash, era, page)` | Pays out a single page of a validator's era rewards. Replaces the unpaged `payout_stakers` for validators with large nominator sets |
| `update_payee(controller)` | Re-reads and re-stores the reward destination for a controller whose payee configuration needs refreshing |
| `deprecate_controller_batch(controllers)` | Governance-only call (not self-service) that migrates a batch of stashes still using a separate controller to the controller-less model. There is no plan to force-migrate legacy controllers this way — migration is expected to happen via individual stash owners calling `set_controller` |
| `restore_ledger(stash, maybe_controller, maybe_total, maybe_unlocking)` | Admin call to repair a corrupted staking ledger |
| `migrate_currency(stash)` | Migrates a stash's bonded balance from the old `Currency` locks to `Fungible` holds |
| `manual_slash(validator_stash, era, slash_fraction)` | Admin call to apply a slash directly, outside the normal offence-reporting path |
### 6. `Rewarded` event drops `identity`, adds `dest`
```
// v7.4
Rewarded { identity: IdentityId, stash: AccountId, amount: Balance }
// v8.0
Rewarded { stash: AccountId, dest: RewardDestination, amount: Balance }
```
`dest` is one of `RewardDestination::Staked` (paid to the stash and re-bonded), `::Stash` (paid to the stash, not re-bonded), `::Account(AccountId)` (paid to an arbitrary account), or the deprecated `::Controller` (paid to the controller — still decodable, but no longer settable going forward). `Rewarded` tells you the destination _policy_, not necessarily a single credited account in an easily-parseable way for `Staked`/`Stash` — for a definitive record of which account was actually credited and by how much, use the accompanying `balances.Deposit { who, amount }` event instead (see [Balances & Transfers](./020-balances-and-transfers.mdx)). Every reward payout mints funds via the standard `pallet-balances` deposit path, which always emits `Deposit`, regardless of which `RewardDestination` variant is in effect.
### 7. `PayoutStarted` event gains paging fields, and payouts can span multiple pages
```
// v7.4
PayoutStarted { era_index: EraIndex, validator_stash: AccountId }
// v8.0
PayoutStarted { era_index: EraIndex, validator_stash: AccountId, page: Page, next: Option }
```
In v7.4, a validator's era reward was always a single unpaged payout: one `PayoutStarted` followed by one `Rewarded` per nominator. In v8, large nominator sets are split across multiple pages (via `payout_stakers_by_page`), so a single validator's era reward can now produce **multiple** `PayoutStarted` + `Rewarded` groups — one per page, each with its own `page` index and a `next` field pointing at the next page (or `None` on the last one). Don't assume "one `PayoutStarted` per validator per era" when indexing payouts; group by `(validator_stash, era_index, page)` instead.
Use the new `StakingApi::eras_stakers_page_count(era, validator_stash)` Runtime API to find out how many pages a validator's era reward has, and `StakingApi::pending_rewards(era, account)` to check whether a specific account still has an unpaid page — see [Runtime APIs & RPC](./120-runtime-apis-and-rpc.mdx#stakingapi--method-set-fully-replaced). Unlike the old `staking_getCurve`, none of these new methods have a JSON-RPC wrapper — they're `state_call`-only.
### 8. `set_staking_configs` gains `max_staked_rewards`
```
// v7.4
set_staking_configs(min_nominator_bond, min_validator_bond, max_nominator_count, max_validator_count, chill_threshold, min_commission)
// v8.0
set_staking_configs(min_nominator_bond, min_validator_bond, max_nominator_count, max_validator_count, chill_threshold, min_commission, max_staked_rewards: ConfigOp)
```
Governance-only call; the new parameter caps what share of total issuance can be paid out as staking rewards, tracked in the new `MaxStakedRewards` storage.
### 9. New errors on `Staking`
`InvalidPage`, `ControllerDeprecated`, `CannotRestoreLedger`, `RewardDestinationRestricted`, `NotEnoughFunds`, `VirtualStakerNotAllowed`, `CannotReapStash`, `AlreadyMigrated`, `Restricted`. Match by name — index positions are not stable across this upgrade.
### 10. Removed calls (no replacement)
`update_permissioned_validator_intended_count` and `chill_from_governance` exist on `Staking` in v7.4; in v8.0 the same-named calls exist only on `Validators` (see #1) — calling them on `Staking` no longer works.
### 11. `MaxNominations` constant removed — query the limit via a Runtime API
v7.4 exposed `Staking::MaxNominations` as a queryable runtime constant (`= 16`). v8 replaces it with `type NominationsQuota = pallet_staking::FixedNominationsQuota<16>` — the effective limit is still 16 nominations per nominator, but it's no longer exposed as a simple `Get` constant. The limit is still queryable, just through a different mechanism: call the new `StakingApi::nominations_quota(balance)` Runtime API, which is the correct way to read it now (see [Runtime APIs & RPC](./120-runtime-apis-and-rpc.mdx#stakingapi--method-set-fully-replaced)). If your tooling reads `api.consts.staking.maxNominations`, switch to that Runtime API call; the returned value is currently `16` for any balance.
### 12. Exposure storage is now paged — old exposure maps are no longer populated
**This is the change most likely to silently break a read-only integration.** v8 adopts the upstream _paged-exposure_ model. The old exposure maps are still present in metadata — so any client that reads them keeps compiling and running — but they are **no longer written for eras after the upgrade**:
**Deprecated (no longer populated):** `ErasStakers`, `ErasStakersClipped`
These maps remain in metadata for backwards compatibility and still contain data for historical (pre-upgrade) eras, but nothing writes them for eras after the v8 upgrade — the election result now populates only `ErasStakersOverview` + `ErasStakersPaged`. A query against them for a current/active era **succeeds and returns empty exposure** (`others: []`); it does not error. Any logic that reads them to determine nominator↔validator relationships, active-stake amounts, or "am I staking this era" status will silently see nothing for every post-upgrade era.
Migrate all exposure reads to the paged model:
- `ErasStakersOverview(era, validator) -> Option` → `{ total, own, nominator_count, page_count }`
- `ErasStakersPaged((era, validator, page)) -> Option` → `{ page_total, others }`
A validator's nominator set (`others`) is now split across `page_count` pages of at most `MaxExposurePageSize` (64) entries each. To reconstruct a validator's full exposure you must read the overview for `total`/`own` and concatenate `others` from every page (`0 .. page_count`), e.g. via `api.query.staking.erasStakersPaged.entries(era, validator)`.
**Determining whether an account is actively staking in an era:** iterate the elected validators (`ErasStakersOverview.entries(era)`), read each validator's pages from `ErasStakersPaged`, and check whether the stash appears in any page's `others`. Do **not** rely on `ErasStakersClipped.others`, which previously held this and is now empty for current eras. For unpaid-reward detection specifically, prefer the new `StakingApi::pending_rewards(era, account)` Runtime API. (This mirrors what the runtime itself now does: its own exposure check reads `ErasStakersPaged` for current eras and only falls back to `ErasStakers` for pre-upgrade eras.)
#### Other storage
Remaining new storage backing the paged model: `ClaimedRewards` (which pages of a validator's era reward have been claimed), `VirtualStakers` and `CounterForVirtualStakers` (accounts that stake without a real bonded balance, e.g. via a pooled-staking design — see the `VirtualStakerNotAllowed` error above), and `MaxStakedRewards` (backing `set_staking_configs` above). `Payee` changes from a `ValueQuery` map (missing entries decode to a default) to an `OptionQuery` map (missing entries decode to `None`) — reads must now handle the `None` case (a bond with no payee configured), e.g. `.unwrapOrDefault()` or an explicit `isNone` check, rather than assuming a value is always returned. Entries that used to decode to a default now return `None`.
Removed storage: `OffendingValidators`, `PermissionedIdentity`, `PolymeshStorageVersion`, `SlashingAllowedFor`, `ValidatorCommissionCap` — all governance/permissioning state that moved to `Validators` (see #1).
#### Constants
Removed constants: `FixedYearlyReward`, `MaxNominatorRewardedPerValidator`, `MaxValidatorPerIdentity`, `MaxVariableInflationTotalIssuance` — reward-curve and permissioning parameters that are now owned by `Validators`' config instead. New constants: `MaxExposurePageSize` (64) caps how many nominators fit on one exposure page; `MaxValidatorSet` (1000) caps the validator set size.
`MaxExposurePageSize` is the **replacement concept for the removed `MaxNominatorRewardedPerValidator`**. Rather than clipping a validator's rewarded nominators to a single cap, v8 paginates them: a validator with more than 64 nominators now has its exposure — and therefore its reward payout — split across multiple pages of up to 64 each (see [section 7](#7-payoutstarted-event-gains-paging-fields-and-payouts-can-span-multiple-pages)), instead of the excess being clipped off.
### 13. `Session` pallet: new events, `DisabledValidators` gains severity
Alongside the `SessionKeys` change (BEEFY key, see [Node operators](#node-operators) below), `Session` gains three new events: `NewQueued` (new session keys queued for the next session), `ValidatorDisabled { validator }`, and `ValidatorReenabled { validator }`. The `DisabledValidators` storage changes from a plain list of validator indices to `Vec<(u32, OffenceSeverity)>`, tracking how severely a validator was disabled — which is what makes `ValidatorReenabled` (partial reinstatement after a lesser offence) possible. New constant `KeyDeposit`.
### 14. Automatic reward payout mechanism
Polymesh pays validator/nominator era rewards automatically — operators do not have to call `payout_stakers` themselves. In v7.4 this was driven by a scheduler-based mechanism inside the pallet, exposed through the `payout_stakers_by_system` call. In v8 that scheduler is gone: automatic payouts run in each block's `on_initialized` hook instead, and `payout_stakers_by_system` no longer exists.
The externally observable consequences:
- **`payout_stakers_by_system` is removed** with no direct replacement (the automatic mechanism is now internal). The manual `payout_stakers` / `payout_stakers_by_page` calls are unaffected and still available.
- **Two new `Validators` events** report the outcome of the automatic payout (see table below).
- `RewardPaymentSchedulingInterrupted { account_id, era, error }` remains declared on `Validators`, but no longer reflects an active scheduler.
| New `Validators` event | Meaning |
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `ValidatorPayoutFailed { validator, era, page, error }` | A validator's automatic payout for a given era page failed (carries the `DispatchError`) |
| `AutomaticPayoutFinished { era }` | The automatic payout for an era has finished |
If you index reward payouts, keep grouping the underlying `PayoutStarted` / `Rewarded` / `balances.Deposit` events as described in [section 6](#6-rewarded-event-drops-identity-adds-dest) and [section 7](#7-payoutstarted-event-gains-paging-fields-and-payouts-can-span-multiple-pages) — those are unchanged. What changed is *when* and *how* the automatic payout is triggered, plus the new success/failure events above.
## Node operators
Node-level setup, session keys, and CLI flags are covered in the [Validator Node Guide](/developer-resources/validator-node-guide/) and [Running a Node with Docker](/node/docker), both already updated for v8. Highlights relevant if you're upgrading:
- A fifth session key (**BEEFY**, `ecdsa`) is now required alongside GRANDPA, BABE, I'm Online, and Authority Discovery — generate session keys with `author_rotateKeysWithOwner` (or the container `rotate` utility), which now also requires your stash account ID and returns an ownership `proof`.
- Node network keys are no longer auto-generated — provision and back up a persistent node key explicitly.
- State/block pruning is configured with `--state-pruning` and `--blocks-pruning`; defaults are `256` and `archive-canonical`.
- The `--operator` CLI flag is removed. It was an alias for `--validator` in v7.4; only `--validator` works on v8. Update any startup scripts that used `--operator`.
- `--ws-port` is removed. HTTP and WebSocket JSON-RPC are served from the same endpoint, configured with `--rpc-port`.
See those guides for full steps — this page only covers pallet-level (call/event/error) changes.
## Migration checklist
1. Update `bond` calls to drop the `controller` argument.
2. Update `set_controller` calls to drop the `controller` argument — it now only ever resets the controller to the stash. Legacy stashes with a separate controller keep working unmigrated; there's no self-service call to force it, and no planned forced migration.
3. Update `chill_other` calls to pass a stash instead of a controller.
4. Move any calls to `add_permissioned_validator`, `remove_permissioned_validator`, `change_slashing_allowed_for`, `update_permissioned_validator_intended_count`, `chill_from_governance`, or `set_commission_cap` from `Staking` to `Validators`. Don't wait on the `Nominated` event — it's declared but never emitted. Also stop calling `payout_stakers_by_system` — it's removed with no replacement (see [section 14](#14-automatic-reward-payout-mechanism)).
5. Re-point any event subscription filtered by `section === 'staking'`: the permissioning events (`PermissionedIdentityAdded`, `InvalidatedNominators`, `CommissionCapUpdated`, and the rest listed in [section 1](#1-permissioning-calls-events-and-errors-moved-to-validators)) are now emitted by the `Validators` pallet.
6. Replace `ErasStakers` / `ErasStakersClipped` exposure reads with `ErasStakersOverview` + `ErasStakersPaged` — they still exist in metadata but return empty exposure for every post-upgrade era (see [section 12](#12-exposure-storage-is-now-paged--old-exposure-maps-are-no-longer-populated)). Reconstruct a validator's nominator set by concatenating `others` across all `page_count` pages, and use `StakingApi::pending_rewards`/`eras_stakers_page_count` for reward status.
7. If you page through or index validator payouts, switch to `payout_stakers_by_page` and group by `(validator_stash, era_index, page)` rather than assuming one payout per validator per era — handle the new `page`/`next` fields on `PayoutStarted`.
8. Update `Rewarded` event decoding for the `dest` field replacing `identity`; for a reliable record of which account was actually credited and how much, consume `balances.Deposit` alongside it.
9. If you read the nomination limit from the `Staking::MaxNominations` constant, that constant is gone — the value (16) is unchanged, but query it via the `StakingApi::nominations_quota(balance)` Runtime API instead of a runtime constant.
10. If you query `Staking::Payee` storage directly, handle `None` — it's no longer a `ValueQuery` map.
11. If you operate a validator node, generate a BEEFY session key and review the [Validator Node Guide](/developer-resources/validator-node-guide/) before upgrading.
For the complete literal list of every added/removed/modified call, event, error, storage item, and constant on `Staking`, `Validators`, and `Session`, see the [Full Pallet API Reference](./110-pallet-api-reference.mdx). For the `StakingApi` Runtime API changes (`get_curve` removed, `nominations_quota`/`eras_stakers_page_count`/`pending_rewards` added) and the removal of the `staking_getCurve` JSON-RPC method, see [Runtime APIs & RPC](./120-runtime-apis-and-rpc.mdx).
---
## Native Asset Holdings Changelog - v7.4 to v8.0
This page covers changes to the `Asset`, `Nft`, and `Portfolio` pallets that let an `Account` hold Polymesh assets directly, without a portfolio. It is part of the [v7.4 → v8.0 changelog](./010-overview.mdx). For the `Settlement` pallet (instructions, affirmations, legs), see [Settlement & Instructions](./050-settlement-and-instructions.mdx).
**Audience:** asset issuers, transfer agents, and anyone building tooling against asset ownership, balances, or NFT collections.
---
## Overview
The `AssetHolder` type (`Account | Portfolio(PortfolioId)`) already existed in v7.4, but only the `Nft` pallet used it — an account could already hold NFTs directly via `PortfolioKind::AccountId`, a variant bolted onto the portfolio-kind enum as a non-breaking addition. That path still went through `PortfolioId`, which always carries a `did: IdentityId` field regardless of `kind` — so holding via an account still required that DID linked to the account also be provided instead of a wallet-only model. The `Asset` pallet's per-account balance storage (`AssetBalance`, `LockedBalance`) also already existed in v7.4, unused by any public call.
Chain v8 doesn't remove the DID-linkage requirement — `AssetHolder::Account` still resolves the account to a DID via `Identity::KeyRecords` and errors `IdentityNotFoundForAccountPortfolio` if none is found — but it does give account-based holding a proper, first-class type instead of a portfolio-kind extension.
Chain v8 turns this into a proper first-class concept:
- A new `AssetHolderKind` enum (`Account | DefaultPortfolio | UserPortfolio(PortfolioNumber)`) replaces the `PortfolioKind::AccountId` variant as the way to specify "who should receive this" without needing the caller's identity resolved yet. `PortfolioKind` reverts to its original two variants (`Default`, `User`) and only means an actual portfolio.
- The `Asset` (fungible) pallet is wired up to use `AssetHolder`/`AssetHolderKind` the same way `Nft` already did.
If your integration already handled NFTs held by accounts, the model is now consistent for fungible assets too — and `PortfolioKind` no longer accepts an `AccountId` variant, so any code passing that must switch to the new types.
## Breaking changes
### 1. `issue` / `redeem` take `AssetHolderKind`, not `PortfolioKind`
```
// v7.4
issue(asset_id, amount, portfolio_kind: PortfolioKind)
redeem(asset_id, value, portfolio_kind: PortfolioKind)
// v8.0
issue(asset_id, amount, asset_holder_kind: AssetHolderKind)
redeem(asset_id, value, asset_holder_kind: AssetHolderKind)
```
Same call indices (7, 8), same intent, different type — `AssetHolderKind` is not SCALE-compatible with `PortfolioKind`.
### 2. `controller_transfer` takes an explicit source holder and destination kind
```
// v7.4
controller_transfer(asset_id, value, from_portfolio: PortfolioId)
// v8.0
controller_transfer(asset_id, value, source: AssetHolder, destination_kind: AssetHolderKind)
```
The v7.4 call could only pull funds from a portfolio, to the caller's own portfolio. The v8 call can pull from any `AssetHolder` (account or portfolio) and send to either an account or a portfolio.
### 3. `approve` — new (Allowances)
```
approve(asset_id, spender: AccountId, amount: Balance)
```
Sets a spending allowance for `spender` over the caller's balance, tracked in the new `Allowances` storage (`(owner, spender, asset_id) -> Balance`). Setting `amount` to `0` removes the allowance entry rather than storing a zero. This has no v7.4 equivalent. Two new events accompany it: `Approval { owner, spender, asset_id, amount }` when the allowance is set, and `AllowanceSpent { owner, spender, asset_id, amount_spent, remaining_allowance }` when a spender draws on it.
`approve` on its own doesn't move funds — the allowance is drawn down when the `spender` later calls `Settlement::transfer_funds` naming the owner's account as `from` (see [Settlement & Instructions](./050-settlement-and-instructions.mdx)). Together they emulate the ERC-20 `approve` / `transferFrom` pattern: an owner pre-authorizes a spend, and a third party (typically a smart contract) executes it without needing the owner's signature on the transfer itself.
Query a spender's remaining allowance with the new `AssetApi::allowance(owner, spender, asset_id)` Runtime API rather than decoding `Allowances` storage directly — see [Runtime APIs & RPC](./120-runtime-apis-and-rpc.mdx#assetapi--version-bump-transfer_report-retyped-new-allowance-method). Note that the `asset_transferReport`/`nft_transferReport` JSON-RPC methods are removed entirely in v8 — `AssetApi::transfer_report`/`NFTApi::transfer_report` are now `state_call`-only.
### 4. NFT: `issue_nft` / `redeem_nft` / `controller_transfer`
Same pattern as fungible assets:
```
// v7.4
issue_nft(asset_id, nft_metadata_attributes, portfolio_kind: PortfolioKind)
redeem_nft(asset_id, nft_id, portfolio_kind: PortfolioKind, number_of_keys)
controller_transfer(nfts, source_portfolio: PortfolioId, callers_portfolio_kind: PortfolioKind)
// v8.0
issue_nft(asset_id, nft_metadata_attributes, holdings_kind: AssetHolderKind)
redeem_nft(asset_id, nft_id, holdings_kind: AssetHolderKind, number_of_keys)
controller_transfer(nfts, source: AssetHolder, destination_kind: AssetHolderKind)
```
### 5. `transfer_nft` — new
```
transfer_nft(nfts: NFTs, to: AccountId, memo: Option)
```
Direct account-to-account NFT transfer. Unlike the `Asset` pallet's `transfer_asset` (see below), there is no separate affirm/reject step — the transfer completes immediately.
### 6. NFT ownership storage consolidated
`NFTOwner` (`(AssetId, NFTId) -> PortfolioId`) — already marked "will be deprecated" in v7.4 — is removed in v8. Ownership is tracked entirely through `NFTHolder` and `Owner` (`(AssetId, NFTId) -> AssetHolder`), both of which already existed in v7.4.
### 7. `NFTPortfolioUpdated` event renamed and retyped
```
// v7.4
NFTPortfolioUpdated(IdentityId, NFTs, Option, Option, PortfolioUpdateReason)
// v8.0
NFTHoldingsUpdated(IdentityId, NFTs, Option, Option, HoldingsUpdateReason)
```
`PortfolioUpdateReason` is correspondingly renamed `HoldingsUpdateReason`.
### 8. Portfolio: parameter renames, and `PortfolioNFT` storage restructured
`delete_portfolio(num)` → `delete_portfolio(portfolio_number)`; `rename_portfolio(num, to_name)` → `rename_portfolio(portfolio_number, new_portfolio_name)`. Types and call indices are unchanged — only the parameter names differ, which matters if you build extrinsics with named arguments.
Separately, the `PortfolioNFT` storage item (tracks which NFTs a portfolio holds) changes from a `StorageDoubleMap` to a `StorageNMap<(PortfolioId, AssetId, NFTId), bool>`. The key set is the same; only the storage map type and resulting key encoding change. Relevant only if you decode this storage item directly rather than through a query API.
### 9. Errors
**`Asset`** — new: `InsufficientAllowance`, `InsufficientTokensLocked`, `InvalidTransferInvalidReceiverDID`, `KeyNotFoundForDid`, `SelfOwnershipTransferNotAllowed`, `UnauthorizedHolderKey`. Removed: `InvalidTransferInvalidReceiverCDD`, `InvalidTransferInvalidSenderCDD` (CDD is no longer checked at the transfer level, consistent with the DID-only onboarding model — see [Identity & Onboarding](./060-identity-and-onboarding.mdx)).
**`Nft`** — new: `InvalidNFTTransferInvalidReceiverDID`, `InvalidNFTTransferSenderDidMatchesReceiverDid`, `NFTIsNotLocked`. Removed: `InvalidNFTTransferInvalidReceiverCDD`, `InvalidNFTTransferInvalidSenderCDD`, `InvalidNFTTransferSenderIdMatchesReceiverId` — same CDD-to-DID pattern as `Asset`, plus a rename (`...SenderIdMatchesReceiverId` → `...SenderDidMatchesReceiverDid`).
**`Portfolio`** — removed: `AccountBasedPortfoliosCannotHaveCustodians` (the account-based-portfolio hack this error guarded against is gone now that accounts are first-class `AssetHolder`s, not a special portfolio kind) and `InvalidTransferSenderIdMatchesReceiverId`, with no direct replacement in `Portfolio` itself. Similar self-transfer guards exist elsewhere in v8 (`Asset::SelfOwnershipTransferNotAllowed` above, `Settlement::SenderSameAsReceiver` in [Settlement & Instructions](./050-settlement-and-instructions.mdx)), though these are separate errors on separate pallets, not a renamed version of this one.
## What did _not_ change
- `transfer_asset`, `receiver_affirm_asset_transfer`, and `reject_asset_transfer` on the `Asset` pallet — these already existed in v7.4 at the same call indices (34, 35, 36), with the same signatures. They are not new in v8.
- The `AssetHolder` enum itself (`Account | Portfolio(PortfolioId)`) is unchanged from v7.4.
- The `AssetBalance` and `LockedBalance` storage maps are unchanged from v7.4.
## Migration checklist
1. Replace any use of `PortfolioKind::AccountId` — it no longer exists. Use `AssetHolderKind::Account` for calls that take it, or `AssetHolder::Account` where a concrete holder is required.
2. Update `issue`, `redeem`, `issue_nft`, `redeem_nft` call encoding for the `AssetHolderKind` parameter type.
3. Update `controller_transfer` (both `Asset` and `Nft`) for the new `source`/`destination_kind` parameters.
4. If you track NFT ownership via `NFTOwner` storage or `NFTPortfolioUpdated` events, switch to `Owner`/`NFTHolder` storage and the `NFTHoldingsUpdated` event.
5. Do not assume `transfer_asset` / `receiver_affirm_asset_transfer` / `reject_asset_transfer` are new — they carry over unchanged from v7.4 if you already support them.
6. Update any extrinsic construction that names `Portfolio.delete_portfolio`/`rename_portfolio` parameters by name.
7. Stop matching on the removed CDD-flavored errors (`InvalidTransferInvalidReceiverCDD`/`...SenderCDD`, `InvalidNFTTransferInvalidReceiverCDD`/`...SenderCDD`, `AccountBasedPortfoliosCannotHaveCustodians`) — none exist on v8.
For the complete literal list of every added/removed/modified call, event, error, storage item, and constant on `Asset`, `Nft`, and `Portfolio`, see the [Full Pallet API Reference](./110-pallet-api-reference.mdx). `AssetApi` and `NFTApi`'s `transfer_report` Runtime API methods went through the same `PortfolioId` → `AssetHolder` retyping as the pallet calls above, `AssetApi` gains a new `allowance` method, and the `asset_transferReport`/`nft_transferReport` JSON-RPC wrappers are removed — see [Runtime APIs & RPC](./120-runtime-apis-and-rpc.mdx).
---
## Settlement & Instructions Changelog - v7.4 to v8.0
This page covers changes to the `Settlement` pallet — instructions, legs, affirmations, and receipts. It is part of the [v7.4 → v8.0 changelog](./010-overview.mdx). For the underlying `AssetHolder` model and `Asset`/`Nft`/`Portfolio` pallet changes, see [Native Asset Holdings](./040-native-asset-holdings.mdx).
**Audience:** anyone building or operating settlement/instruction flows directly against the chain.
---
## Overview
An instruction `Leg` in v7.4 could only move assets between portfolios (`sender`/`receiver: PortfolioId`). In v8, `Leg::Fungible` and `Leg::NonFungible` use `AssetHolder` for `sender`/`receiver`, so a leg can move assets directly to or from an account. Every call that lets a party affirm, reject, or withdraw against a set of portfolios is updated to work against a set of `AssetHolder`s instead.
## Breaking changes
### 1. `withdraw_affirmation` and its variants removed
`withdraw_affirmation`, `withdraw_affirmation_with_count`, and `withdraw_affirmation_as_mediator` are removed with no replacement call. Use `reject_instruction` (or its `_with_count`/`_as_mediator` variants) instead.
The `AffirmationWithdrawn` event still exists in the `Event` enum for type-compatibility reasons but is no longer emitted by anything in v8.
### 2. `portfolios` / `portfolio` parameters renamed to `holder_set` / `asset_holder`
| Call | v7.4 parameter | v8.0 parameter |
| ------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------- |
| `affirm_instruction` | `portfolios: BoundedBTreeSet` | `holder_set: BoundedBTreeSet` |
| `affirm_instruction_with_count` | same | same |
| `add_and_affirm_instruction` | same | same |
| `add_and_affirm_with_mediators` | same | same |
| `reject_instruction` | `portfolio: PortfolioId` | `asset_holder: AssetHolder` |
| `reject_instruction_with_count` | same | same |
| `execute_manual_instruction` | `portfolio: Option` | `asset_holder: Option` |
`add_instruction` and `add_and_affirm_instruction` themselves are unchanged apart from this — legs already carry their own sender/receiver, so only the affirming party's holder set moves.
The `MaxNumberOfPortfolios` runtime constant is replaced by `MaxNumberOfAssetHolders`. Both the `SettlementApi::get_affirmation_count` Runtime API and the `settlement_getAffirmationCount` JSON-RPC method go through the same rename/retype (`portfolios: Vec` → `holder_set: Vec`) — see [Runtime APIs & RPC](./120-runtime-apis-and-rpc.mdx#settlementapi--version-bump-get_affirmation_count-retyped-new-get_receiver_affirmation_requirement-method).
### 3. `Leg` sender/receiver: `PortfolioId` → `AssetHolder`
```
// v7.4
Leg::Fungible { sender: PortfolioId, receiver: PortfolioId, asset_id, amount }
Leg::NonFungible { sender: PortfolioId, receiver: PortfolioId, nfts }
// v8.0
Leg::Fungible { sender: AssetHolder, receiver: AssetHolder, asset_id, amount }
Leg::NonFungible { sender: AssetHolder, receiver: AssetHolder, nfts }
```
`Leg::OffChain` is unchanged.
### 4. `ReceiptDetails` gains a required expiry
```
// v7.4
ReceiptDetails { uid, instruction_id, leg_id, signer, signature, metadata }
// v8.0
ReceiptDetails { uid, instruction_id, leg_id, signer, signature, expires_at: Moment, metadata }
```
Off-chain affirmation receipts must carry an expiry on v8. `affirm_with_receipts` and `affirm_with_receipts_with_count` take the updated `ReceiptDetails` type.
### 5. Events retyped from `PortfolioId` to `AssetHolder`
`InstructionAffirmed`, `AffirmationWithdrawn` (see #1), and `InstructionAutomaticallyAffirmed` all change their second field from `PortfolioId` to `AssetHolder`. The same swap happens in storage: `AffirmsReceived` re-keys from `(InstructionId, PortfolioId)` to `(InstructionId, AssetHolder)`, and `UserAffirmations` from `(PortfolioId, InstructionId)` to `(AssetHolder, InstructionId)`.
### 6. New errors
`ReceiptExpired`, `SenderSameAsReceiver`, `AllowancesNotSupportedForNFTs`, `InstructionAlreadyLocked`, `InstructionNotLocked`, `RelockCooldownNotExpired`, `MaxRelockCountExceeded`, `MissingInstructionMediators`.
### 7. `create_venue` and `update_venue_signers`: `signers` type change
```
// v7.4
create_venue(details, signers: Vec, typ)
update_venue_signers(id, signers: Vec, add_signers: bool)
// v8.0
create_venue(details, signers: BTreeSet, typ)
update_venue_signers(id, signers: BTreeSet, add_signers: bool)
```
`BTreeSet` deduplicates and sorts; if you passed unsorted or duplicate signers before, behavior changes. The `VenueSignersUpdated` event's `signers` field changes from `Vec` to `BTreeSet` to match.
### 8. Removed events: `SchedulingFailed`, `InstructionRescheduled`
Both are removed with no replacement event. They were tied to the old ad hoc rescheduling behavior for failed instruction execution, which v8 replaces with the explicit lock/unlock/relock model (`unlock_instruction`, described under [New features](#new-features) below) rather than automatic rescheduling.
## New features
### `set_mandatory_receiver_affirmation`
```
set_mandatory_receiver_affirmation(requirement: AffirmationRequirement)
```
By default on v8, receiver affirmation is automatic. An identity can call this to require explicit affirmation from receivers instead, recorded in the new `MandatoryReceiverAffirmation` storage and announced via the new `MandatoryReceiverAffirmationSet` event.
Check whether a given receiver/asset pair currently requires explicit affirmation with the new `SettlementApi::get_receiver_affirmation_requirement(receiver, asset_id)` Runtime API before submitting an instruction — see [Runtime APIs & RPC](./120-runtime-apis-and-rpc.mdx#settlementapi--version-bump-get_affirmation_count-retyped-new-get_receiver_affirmation_requirement-method).
### `unlock_instruction`
Pairs with `lock_instruction`, which already existed in v7.4. New storage `UnlockedTimestamp` and `InstructionRelockCount` support a lock/unlock/relock cycle: an instruction can be relocked up to `MaxRelockCount` times (3 on Mainnet/Testnet), with a `RelockCooldown` of 4 hours between relocks. New event `InstructionUnlocked`.
### `transfer_funds`
```
transfer_funds(from: Option, to: AssetHolder, fund: Fund)
```
Moves a `Fund` (fungible amount or a set of NFTs) directly between two `AssetHolder`s — account or portfolio, on either side — without creating a settlement instruction. Emits `FundsTransferred(IdentityId, AssetHolder, AssetHolder, Fund)`. This is the `Settlement` pallet's equivalent of `Asset::transfer_asset` and `Nft::transfer_nft`, but generalized to cover portfolio-to-portfolio and mixed moves as well.
If the caller isn't the owner of an account-based `from`, `transfer_funds` draws down the allowance set by `Asset::approve` instead of requiring the owner's signature (fails with `AllowancesNotSupportedForNFTs` for NFT funds, which have no allowance concept). This is what lets `approve` + `transfer_funds` emulate ERC-20-style `approve`/`transferFrom` — see [Native Asset Holdings](./040-native-asset-holdings.mdx) for the `approve` side.
## Migration checklist
1. Stop calling `withdraw_affirmation` / `withdraw_affirmation_with_count` / `withdraw_affirmation_as_mediator` — use the corresponding `reject_instruction*` call instead.
2. Rename `portfolios`/`portfolio` arguments to `holder_set`/`asset_holder` and change their type from `PortfolioId`/`BoundedBTreeSet` to `AssetHolder`/`BoundedBTreeSet` wherever you build these calls.
3. Update any code constructing or decoding `Leg::Fungible`/`Leg::NonFungible` for `AssetHolder` sender/receiver.
4. Add an `expires_at` value when building off-chain affirmation receipts.
5. Update `InstructionAffirmed`/`InstructionAutomaticallyAffirmed` event decoding for the `AssetHolder` field; stop listening for `AffirmationWithdrawn`.
6. If you require explicit receiver affirmation rather than the new default automatic behavior, call `set_mandatory_receiver_affirmation`.
7. Update `create_venue` and `update_venue_signers` calls (and `VenueSignersUpdated` event decoding) that build/read `signers` as a `Vec` to use a `BTreeSet` instead.
8. Stop listening for `SchedulingFailed`/`InstructionRescheduled` — neither is emitted on v8.
For the complete literal list of every added/removed/modified call, event, error, storage item, and constant on `Settlement`, see the [Full Pallet API Reference](./110-pallet-api-reference.mdx). For the new `SettlementApi::get_receiver_affirmation_requirement` Runtime API, see [Runtime APIs & RPC](./120-runtime-apis-and-rpc.mdx).
---
## Identity & Onboarding Changelog - v7.4 to v8.0
This page covers changes to the `Identity` pallet and the group pallet instance formerly known as `CddServiceProviders`. It is part of the [v7.4 → v8.0 changelog](./010-overview.mdx). For the conceptual model (why DID-only onboarding, what a DID registrar is), see [Identity Onboarding](/identity/onboarding) and [Roles](/identity/roles) — this page only covers the pallet-level call/event/error/storage changes.
**Audience:** anyone registering identities, verifying onboarding status, or managing DID registrars directly against the chain.
---
## Overview
Chain v8 completes a rename that runs through the whole pallet: **CDD service providers become DID registrars**. This is not a cosmetic rename — v7.4 already allowed accounts to hold POLYX and stake without a DID (see the [Balances & Transfers](./020-balances-and-transfers.mdx) changelog), and v8 extends that by adding a way to obtain a DID itself without going through a CDD-attesting authority: `self_register_did`. The old registration calls (`cdd_register_did`, `cdd_register_did_with_cdd`) still exist and are still callable — they are not removed — but they are deprecated (`since = "8.0.0"`) and no longer attach a `CustomerDueDiligence` claim: both now forward to the same claim-free registration path as `register_did`. They are retained only because, unlike `register_did`, they still accept a `secondary_keys` argument.
Child identities are also removed in v8.
## Breaking changes
### 1. `CddServiceProviders` renamed to `DidRegistrars`
The underlying pallet is unchanged — it's the same `pallet_group` instance, with identical calls (`set_active_members_limit`, `disable_member`, `add_member`, `remove_member`, `swap_member`, `reset_members`, `abdicate_membership`). Only the runtime name changes: calls, events, and errors that were routed through `CddServiceProviders` are now routed through `DidRegistrars`. Anything that references the pallet by name string breaks.
`Identity::Config::CddServiceProviders` is also renamed to `Identity::Config::DidRegistrars`, and `Identity::Config::CddHandler` is renamed to `Identity::Config::TxFeeHandler`.
### 2. `accept_primary_key` and `rotate_primary_key_to_secondary` drop the CDD authorization argument
```
// v7.4
accept_primary_key(rotation_auth_id: u64, optional_cdd_auth_id: Option)
rotate_primary_key_to_secondary(auth_id: u64, optional_cdd_auth_id: Option)
// v8.0
accept_primary_key(rotation_auth_id: u64)
rotate_primary_key_to_secondary(auth_id: u64)
```
The `Identity` authorization these calls used to consume, `AuthorizationData::AttestPrimaryKeyRotation` (a DID registrar's attestation of a primary-key rotation, formerly a CDD provider's), is now formally deprecated (`#[deprecated(note = "This authorization variant is no longer used")]`). It is **retained in the enum, not removed** (so the variant index is unchanged and pre-upgrade authorizations of this type still decode), but no v8 call creates one and `accept_primary_key` no longer consumes one. The matching `AuthorizationType::AttestPrimaryKeyRotation` is likewise retained-but-deprecated. This is the authorization-layer counterpart to the removed `change_cdd_requirement_for_mk_rotation` call and `CddAuthForPrimaryKeyRotation` storage below — stop issuing this authorization type; new code should not create it.
### 3. `cdd_register_did` and `cdd_register_did_with_cdd` no longer attach a CDD claim
Both calls are deprecated (`#[deprecated(since = "8.0.0", ...)]`) but still exist and are still registrar-gated. However their behavior changed: in v7.4 they added a `Claim::CustomerDueDiligence` claim as part of registration; in v8 they forward to the same claim-free registration path as `register_did`, so **no claim is added**. `cdd_register_did_with_cdd`'s `_expiry` argument is accepted but ignored. The only remaining reason to call these over `register_did` is that they accept a `secondary_keys` argument. To attach a `CustomerDueDiligence` claim in v8, use `add_claim` (registrar-gated for that claim type) or `gc_add_cdd_claim`.
### 4. Removed calls, with no replacement
- `invalidate_cdd_claims`
- `change_cdd_requirement_for_mk_rotation`
- `create_child_identity`
- `create_child_identities`
- `unlink_child_identity`
Both the `IdentityApi` Runtime API and its JSON-RPC wrappers lose the corresponding methods in step with this shift: `IdentityApi::is_identity_has_valid_cdd`/`identity_isIdentityHasValidCdd` and `IdentityApi::valid_cdd_claims`/`identity_validCDDClaims` are all gone in v8 — see [Runtime APIs & RPC](./120-runtime-apis-and-rpc.mdx#identityapi--two-methods-removed).
### 5. Renamed errors
| v7.4 | v8.0 |
| ------------------------------------- | -------------------------------------- |
| `UnAuthorizedCddProvider` | `UnAuthorizedDidRegistrar` |
| `InvalidAuthorizationFromCddProvider` | `InvalidAuthorizationFromDidRegistrar` |
| `NotCddProviderAttestation` | `NotDidRegistrarAttestation` |
| `TargetHasNoCdd` | `TargetDidInactive` |
| `UnauthorizedCallerDidMissingCdd` | `UnauthorizedCallerDidInactive` |
### 6. Removed errors
`IsChildIdentity`, `NoParentIdentity`, `NotParentOrChildIdentity` (child identities), and `AccountBasedPortfoliosCannotHaveCustodians` (superseded by the `AssetHolder` model — see [Native Asset Holdings](./040-native-asset-holdings.mdx)).
### 7. Removed events
- `CddRequirementForPrimaryKeyUpdated` (paired with the removed `change_cdd_requirement_for_mk_rotation` call)
- `CddClaimsInvalidated` (paired with the removed `invalidate_cdd_claims` call)
- `ChildDidCreated`, `ChildDidUnlinked` (child identities)
- `AssetDidRegistered` (legacy ticker-to-DID linkage)
### 8. Removed storage
`CddAuthForPrimaryKeyRotation`, `CurrentPayer`, `ChildDid`, `ParentDid`.
## New features
### `register_did` and `self_register_did`
```
register_did(target_account: AccountId)
self_register_did()
```
`register_did` is callable only by an active DID registrar, and registers a DID for `target_account` with no secondary keys and no CDD claim. `self_register_did` is fully permissionless — any account not already linked to an identity can call it to create its own DID, again with no CDD claim. Per the pallet source: _"This allows users to self onboard without needing to go through a DID registrar (formerly CDD provider). No CDD claim is added — DID existence is sufficient for onboarding."_
## What did _not_ change
- `cdd_register_did` and `cdd_register_did_with_cdd` still exist, are still registrar-gated, and still accept a `secondary_keys` argument (the one capability `register_did` doesn't have) — but see the breaking behavior change below: they no longer attach a `CustomerDueDiligence` claim.
- `gc_add_cdd_claim` / `gc_revoke_cdd_claim` (governance-council-administered CDD claims) are unchanged.
- The `CustomerDueDiligence` claim variant itself still exists in the `Claim` enum — CDD as a concept is not removed, only the requirement that basic onboarding go through it.
- `pallets/group` (the crate backing `DidRegistrars`) has no functional changes.
## Migration checklist
1. Update any code that references the `CddServiceProviders` pallet by name to `DidRegistrars`.
2. Drop the `optional_cdd_auth_id` argument from `accept_primary_key` and `rotate_primary_key_to_secondary` calls.
3. If you rely on `cdd_register_did`/`cdd_register_did_with_cdd` attaching a `CustomerDueDiligence` claim, that no longer happens in v8 — add the claim separately via `add_claim` or `gc_add_cdd_claim` if still required.
4. Stop calling `invalidate_cdd_claims`, `change_cdd_requirement_for_mk_rotation`, `create_child_identity`, `create_child_identities`, or `unlink_child_identity` — none exist on v8.
5. Update error handling for the CDD→DID-registrar error renames (#5 above), and remove handling for the child-identity errors.
6. If you need a lightweight, no-CDD-claim path to onboard new identities, use `self_register_did` (permissionless) or `register_did` (DID-registrar-gated) instead of building a claim-issuing flow.
7. If you rely on `Identity::Config::CddServiceProviders` or `::CddHandler` in custom runtime code, update to `::DidRegistrars` / `::TxFeeHandler`.
For the complete literal list of every added/removed/modified call, event, error, storage item, and constant on `Identity`, see the [Full Pallet API Reference](./110-pallet-api-reference.mdx). For the `IdentityApi` Runtime API and JSON-RPC method removals, see [Runtime APIs & RPC](./120-runtime-apis-and-rpc.mdx).
---
## Relayer & Subsidies Changelog - v7.4 to v8.0
This page covers changes to the `Relayer` pallet (subsidies, fee-paying keys) and the `relay_tx` call, which moves from `Utility` to `Relayer`. It is part of the [v7.4 → v8.0 changelog](./010-overview.mdx).
**Audience:** anyone subsidizing accounts' transaction fees, or relaying signed calls on behalf of another account.
---
## Overview
In v7.4, granting a subsidy went through the generic `Identity` authorization system: `set_paying_key` created an `AddRelayerPayingKey` authorization, which the user key then accepted via `accept_paying_key(auth_id)` — the same generic accept/reject/consume mechanism used for key rotation, ownership transfers, and other authorization types.
In v8, subsidies get a dedicated state machine, native to the `Relayer` pallet: `approve_subsidy` writes directly into a new `PendingSubsidies` storage, and the user accepts with `accept_subsidy(paying_key)` — no authorization ID involved.
`relay_tx` also moves from `Utility` to `Relayer` in v8, gaining an `expires_at` field alongside the nonce mechanism it already had.
## Breaking changes
### 1. Subsidy grant/accept flow replaced
| v7.4 | v8.0 | Notes |
| ----------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `set_paying_key(user_key, polyx_limit)` | `approve_subsidy(user_key, polyx_limit)` | Renamed. No longer creates an `Identity` authorization — writes directly to `PendingSubsidies` |
| `accept_paying_key(auth_id: u64)` | _(removed)_ | Replaced by `accept_subsidy` below |
| — | `accept_subsidy(paying_key)` | New. User accepts by naming the paying key directly, no `auth_id` |
| — | `revoke_subsidy(user_key)` | New. Subsidizer cancels a pending (not yet accepted) subsidy |
| `remove_paying_key(user_key, paying_key)` | `remove_subsidy(user_key, paying_key)` | Renamed, signature unchanged. Callable by either party, same as v7.4 |
`update_polyx_limit`, `increase_polyx_limit`, and `decrease_polyx_limit` are unchanged in name and signature — only their call indices shift, because of the two new calls inserted before them.
### 2. `relay_tx` moves from `Utility` to `Relayer`, and gains an expiry
```
// v7.4 - Utility::relay_tx
relay_tx(target: AccountId, signature: OffChainSignature, call: UniqueCall)
// UniqueCall wraps the call with a nonce the caller must supply, checked against Utility's Nonces storage
// v8.0 - Relayer::relay_tx
relay_tx(target: AccountId, signature: OffChainSignature, call: Box, expires_at: Moment)
// The nonce is no longer a caller-supplied argument -- it's tracked server-side in the new
// Relayer::RelayTxNonces storage (auto-incremented per target) and mixed into the signed
// message alongside expires_at
```
`Utility::relay_tx` and its `Nonces` storage are removed entirely, replaced by `Relayer::relay_tx` and the new `RelayTxNonces` storage. Replay protection is still nonce-based underneath, but the nonce is no longer an explicit call argument you supply and check yourself — it's resolved from chain state when building the signed payload, and `expires_at` is a new, separate expiry check on top. Either way, signed relay payloads built for v7.4 will not work on v8 — the call moved pallets and the payload shape changed.
### 3. Events renamed and retyped
| v7.4 | v8.0 |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `AuthorizedPayingKey(EventDid, AccountId, AccountId, Balance, u64)` | `ApprovedSubsidy { user_key, paying_key, initial_polyx_limit }` |
| `AcceptedPayingKey(EventDid, AccountId, AccountId)` | `AcceptedSubsidy { user_key, paying_key, initial_polyx_limit }` |
| `RemovedPayingKey(EventDid, AccountId, AccountId)` | `RemovedSubsidy { user_key, paying_key, remaining }` |
| `UpdatedPolyxLimit(EventDid, AccountId, AccountId, Balance, Balance)` | `UpdatedPolyxLimit { user_key, paying_key, remaining, old_remaining }` |
All v8 events drop the identity field that every v7.4 event carried, and use named struct fields instead of positional tuples.
### 4. New events
`RemovedPendingSubsidy { user_key, paying_key, initial_polyx_limit }` (fired by `revoke_subsidy`), `SubsidyDebited { user_key, paying_key, amount }` (fired when a subsidized transaction consumes POLYX from the subsidy), `RelayedTx { caller, target, result }` (fired by `relay_tx`).
### 5. Errors changed
Removed: `UserKeyCddMissing`, `PayingKeyCddMissing` (no CDD requirement in the new flow), `NotAuthorizedForPayingKey`, `NotAuthorizedForUserKey`, `BadAuthorizationType`, `IdentityNotFound` (all specific to the old authorization-based flow).
New: `NoPendingSubsidy` (returned by `accept_subsidy`/`revoke_subsidy` when there is nothing pending), `InvalidSignature` and `ExpiredRelayTx` (both for `relay_tx`).
Unchanged: `NoPayingKey`, `NotPayingKey`, `Overflow`.
On `Utility`: `InvalidSignature`, `TargetCddMissing`, and `InvalidNonce` are removed along with `relay_tx` itself. `UnableToDeriveAccountId` remains, used elsewhere in `Utility`.
### 6. `AuthorizationType::AddRelayerPayingKey` renamed to `OldAddRelayerPayingKey`
The `Identity` pallet's `AuthorizationData` variant used by the old flow is renamed, not removed — this preserves the ability to decode pre-upgrade authorizations still sitting in storage. New code should not create this authorization type; use `approve_subsidy` instead.
## Migration checklist
1. Replace `set_paying_key` with `approve_subsidy` and `remove_paying_key` with `remove_subsidy` (same arguments).
2. Replace `accept_paying_key(auth_id)` with `accept_subsidy(paying_key)` — stop tracking or waiting on an authorization ID for subsidy acceptance.
3. If you cancel subsidies before acceptance, use the new `revoke_subsidy`.
4. Move any `relay_tx` usage from `Utility` to `Relayer`, add an `expires_at` value when building the signed payload, and stop supplying a nonce yourself — it's now resolved from `Relayer::RelayTxNonces` rather than being a call argument.
5. Update event decoding for the new named-field shapes and the identity field removal; handle the new `RemovedPendingSubsidy` and `SubsidyDebited` events if you track subsidy lifecycle or spend.
6. Remove handling for CDD-related and authorization-related Relayer errors that no longer exist; add handling for `NoPendingSubsidy`, `InvalidSignature`, `ExpiredRelayTx`.
For the complete literal list of every added/removed/modified call, event, error, storage item, and constant on `Relayer` and `Utility`, see the [Full Pallet API Reference](./110-pallet-api-reference.mdx).
---
## Smart Contracts Changelog - v7.4 to v8.0
This page covers `pallet-revive`, the smart contract pallet on Polymesh for v8. It is part of the [v7.4 → v8.0 changelog](./010-overview.mdx). See [Smart Contracts](/development/smart-contracts) for the conceptual guide to Polymesh smart contracts, including PolkaVM vs EVM, the `eth-rpc` proxy, and how to build tooling against it.
**Audience:** smart contract developers and anyone building tooling that deploys or calls contracts on Polymesh.
---
## Wasm/ink! contracts (`pallet-contracts`, `polymesh_contracts`) removed
The legacy Wasm/ink! contracts pallets — `pallet-contracts` and Polymesh's `polymesh_contracts` wrapper — are completely removed in v8. Neither pallet is part of the v8 runtime. `pallet-revive` is now the only smart contract pallet on Polymesh. This isn't a breaking removal in practice, since no contracts were ever deployed on Mainnet through `pallet-contracts` in the first place.
## `pallet-revive` (new)
`pallet-revive` has no v7.4 equivalent — it is a new pallet in the mainnet runtime, from Polymesh's `polkadot-sdk` fork. It is a **dual-VM contract execution engine**: it runs two virtual machines, and which one executes a contract depends on how that contract was compiled.
- **PolkaVM** (RISC-V) executes PolkaVM bytecode. Solidity compiled with the `resolc` compiler (instead of `solc`) targets PolkaVM, but does **not** get full EVM compatibility.
- **revm** (a Rust EVM) executes standard EVM bytecode. Solidity compiled with `solc` deploys and runs in `revm` with **full EVM compatibility**.
So EVM support is not merely an "interface" onto PolkaVM — EVM bytecode runs in a separate VM (`revm`) from PolkaVM contracts. The `AllowEVMBytecode` config flag (below) toggles the `revm` execution path; it is `true` on all Polymesh networks.
Which VM runs a contract is decided by the **uploaded code** (RISC-V/PolkaVM bytecode vs EVM bytecode), not by the submission path. EVM-bytecode (Solidity/`solc`) contracts can therefore be deployed and called via ordinary **Substrate transactions** (`instantiate_with_code` / `call` from e.g. polkadot.js or subxt), not only through the `eth-rpc` proxy — the Ethereum JSON-RPC path is a convenience for standard ETH tooling, not a requirement. See the [Smart Contracts](/development/smart-contracts) conceptual guide for the compiler/tooling detail.
### Address mapping
Polymesh accounts are 32-byte (`AccountId32`), not 20-byte Ethereum addresses, so the runtime uses `AddressMapper = pallet_revive::AccountId32Mapper`. Every existing Polymesh account already has an implicit, deterministic Ethereum address derived from it. Because deriving a 20-byte address from a 32-byte account is lossy, the pallet stores the original account (`OriginalAccount`) once an account opts in via the `map_account` call, making the mapping reversible. `unmap_account` reverses this.
### Calls
`pallet-revive` exposes: `eth_transact` (submit a raw signed Ethereum transaction), `call`, `instantiate`, `instantiate_with_code`, `eth_instantiate_with_code`, `eth_call`, `eth_substrate_call`, `upload_code`, `remove_code`, `set_code`, `map_account`, `unmap_account`, and `dispatch_as_fallback_account`. These are the standard upstream `pallet-revive` calls — Polymesh has not added or removed any at the pallet level; the customization is entirely in runtime configuration (below).
### Runtime configuration
Values set in `impl pallet_revive::Config for Runtime`:
| Setting | Value | Notes |
| ------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `ChainId` | `1_641_820` (Mainnet), `1_641_819` (Testnet), `1_641_818` (Develop) | The EVM chain ID wallets and tooling (MetaMask, ethers.js) need to target the right network |
| `NativeToEthRatio` | `10^12` | Bridges Polymesh's 6-decimal POLYX to Ethereum's 18-decimal wei convention |
| `AllowEVMBytecode` | `true` | Toggles the `revm` EVM-bytecode execution path, separate from native PolkaVM contracts |
| `GasScale` | `100` | Scales EVM gas to Polymesh's weight-based fee model |
| `Precompiles` | `()` | No custom precompiles configured beyond the pallet's defaults |
| `AddressMapper` | `AccountId32Mapper` | See above |
### Errors
`pallet-revive` has no v7.4 equivalent to diff against, so this is its full error surface in v8, grouped by theme:
**Contract/code lookup**
| Error | Meaning |
| ------------------------- | ------------------------------------------------------------------ |
| `ContractNotFound` | No contract at the specified address |
| `CodeNotFound` | No code at the supplied code hash |
| `CodeInfoNotFound` | No code info at the supplied code hash |
| `CodeRejected` | The contract failed to compile or is missing required entry points |
| `CodeInUse` | Code removal denied — still in use by at least one contract |
| `DuplicateContract` | A contract with the same account ID already exists |
| `RefcountOverOrUnderflow` | A code hash's reference count over- or under-flowed |
**Execution & gas**
| Error | Meaning |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `OutOfGas` | The contract exhausted its gas limit |
| `MaxCallDepthReached` | Call depth exceeded the schedule's limit |
| `ContractTrapped` | The contract trapped during execution |
| `ExecutionFailed` | PolkaVM failed during execution, likely a malformed program |
| `InvalidInstruction` | The program contains an invalid instruction |
| `InvalidJump` | Dynamic jump targeted an invalid destination |
| `StackUnderflow` / `StackOverflow` | Popped from an empty stack / pushed onto a full one |
| `BasicBlockTooLarge` | A basic block exceeds the allowed size |
| `TransferFailed` | The requested transfer failed, likely insufficient free balance |
| `ContractReverted` | The contract completed but reverted its storage changes (extrinsic calls only — direct/RPC calls return `Ok` and require inspecting the flags) |
**Calls & reentrancy**
| Error | Meaning |
| -------------------------- | ---------------------------------------------------------------------- |
| `InvalidCallFlags` | Invalid flag combination passed to a call/delegate-call |
| `TerminatedWhileReentrant` | Termination denied while the contract is already on the call stack |
| `TerminatedInConstructor` | A contract self-destructed in its constructor |
| `InputForwarded` | Input was already forwarded by a prior call and is no longer available |
| `ReentranceDenied` | Called a contract flagged as non-reentrant |
| `ReenteredPallet` | A contract called back into `pallet-revive` from the runtime |
| `PrecompileDelegateDenied` | Delegate-called a precompile that disallows it |
**Storage & deposits**
| Error | Meaning |
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `OutOfBounds` | A buffer outside sandbox memory was passed to a contract API |
| `DecodingFailed` | Input failed to decode as the expected type |
| `ValueTooLarge` | An event body or storage item exceeds the size limit |
| `StateChangeDenied` | A state-modifying API was invoked in read-only mode |
| `StorageDepositNotEnoughFunds` | Insufficient balance to pay the required storage deposit |
| `StorageDepositLimitExhausted` | More storage was created than the deposit limit allows |
| `StorageRefundNotEnoughFunds` | The contract lacks balance to refund a storage deposit (should never happen — an accounting bug if seen) |
| `StorageRefundLocked` | A lock on the contract's storage deposit prevents refunding it |
| `OutOfTransientStorage` | No room left in transient storage |
| `InvalidStorageFlags` | Invalid flags passed to a storage syscall |
| `CallDataTooLarge` / `ReturnDataTooLarge` | Call data or return data exceeds the size limit |
| `TooManyTopics` | Too many topics passed to the event-deposit API |
**Code/blob validation**
| Error | Meaning |
| ---------------------- | ---------------------------------------------------------------------- |
| `InvalidSchedule` | Invalid schedule, e.g. zero weight for a basic operation |
| `BlobTooLarge` | The code blob exceeds the size limit |
| `StaticMemoryTooLarge` | The contract declares too much memory (read-only + read-write + stack) |
**Delegate dependencies**
| Error | Meaning |
| ----------------------------------- | --------------------------------------------------------------- |
| `MaxDelegateDependenciesReached` | The contract hit its maximum number of delegate dependencies |
| `DelegateDependencyNotFound` | The dependency isn't in the contract's delegate-dependency list |
| `DelegateDependencyAlreadyExists` | Already depends on the given delegate dependency |
| `CannotAddSelfAsDelegateDependency` | Can't add a contract's own code hash as its delegate dependency |
**Account mapping & syscalls**
| Error | Meaning |
| ------------------------ | --------------------------------------------------------------------------------------- |
| `AccountUnmapped` | An `AccountId32` account tried to interact without a mapping — call `map_account` first |
| `AccountAlreadyMapped` | Tried to map an account that's already mapped |
| `InvalidSyscall` | Called a syscall that doesn't exist at the current API level |
| `InvalidImmutableAccess` | Immutable data can only be set on deploy and read on calls — set once, non-empty |
**EVM / Ethereum compatibility**
| Error | Meaning |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| `InvalidGenericTransaction` | The transaction used to dry-run a contract is invalid |
| `BalanceConversionFailed` | Failed to convert a `U256` to a `Balance` |
| `EvmConstructorNonEmptyData` | EVM constructors don't accept separate `data` — input is part of the code blob |
| `EvmConstructedFromHash` | EVM contracts can only be instantiated via code upload, not by code hash — there's no on-chain initcode |
| `UnsupportedPrecompileAddress` | The precompile address isn't supported |
| `EcdsaRecoveryFailed` | ECDSA public key recovery failed — likely a wrong recovery ID or signature |
| `TxFeeOverdraw` | Too much deposit drawn from the shared tx-fee/deposit credit — the `gas` passed in the Ethereum transaction was too low |
### Running EVM JSON-RPC
`pallet-revive` is a Substrate pallet, not a full Ethereum node — it doesn't speak Ethereum's JSON-RPC (`eth_call`, `eth_sendRawTransaction`, `eth_getBalance`, etc.) directly. A separate proxy process, `pallet-revive-eth-rpc` (binary name `eth-rpc`), new in v8, translates standard Ethereum JSON-RPC into calls against the node. Run it alongside your node to point MetaMask, ethers.js, or other standard Ethereum tooling at Polymesh. It is only needed for that Ethereum-tooling path — EVM contracts can also be deployed and called directly via Substrate transactions (see the note under [`pallet-revive`](#pallet-revive-new) above).
## Migration checklist
1. Target `pallet-revive` for all contract work on Polymesh.
2. To interact with Polymesh contracts using standard Ethereum tooling, run the `eth-rpc` proxy alongside your node and use the correct `ChainId` for your network.
3. Have users call `map_account` if you need a reversible EVM-address mapping for their Polymesh account (for example, to look up their address in a block explorer).
4. Handle `pallet-revive`'s error set (above) if you're building a wallet, indexer, or dApp framework against it — most are execution/storage-limit conditions specific to the PolkaVM environment.
`pallet-revive` is a wholesale-new pallet, so it isn't itemized in the [Full Pallet API Reference](./110-pallet-api-reference.mdx) (a name-based diff against v7.4) — this page and its Errors section above are the reference for it.
---
## Confidential Assets Changelog - v7.4 to v8.0
This page covers the new `ConfidentialAssets` pallet (Polymesh Confidential Assets, "PCA"). It is part of the [v7.4 → v8.0 changelog](./010-overview.mdx). For the concepts, cryptography, and workflows, see the [Confidential Assets](/confidential-assets) documentation — this page only covers chain-level facts (availability, call surface, pallet identity), and links to the conceptual docs rather than re-explaining them.
**Audience:** anyone evaluating or building against Polymesh Confidential Assets.
---
## Status: not on Mainnet, pre-release
`pallet_confidential_assets` is genuinely new in v8 — there is no v7.4 equivalent to diff against. It is a pre-release feature that has not yet completed a comprehensive code audit, and it is **not included in the Mainnet runtime at all**: `ConfidentialAssets` does not appear in `pallets/runtime/mainnet/src/runtime.rs`'s pallet list. It is included in the Testnet and Develop runtimes (pallet index 70 on Testnet). Previously it was only available on an internal Devnet; v8 is what makes it reachable on Testnet.
Do not build production integrations against Confidential Assets yet, and expect the pallet's interface to continue changing before it reaches Mainnet.
## What it is
Confidential Assets provides sender/receiver/asset/value-confidential transfers using zero-knowledge proofs, while still supporting regulated-market workflows (receiver affirmation, multi-leg settlement, asset-specific auditor/mediator compliance access). The underlying protocol is documented in the [P-DART paper](https://assets.polymesh.network/P-DART-v1.pdf), building on [DART](https://eprint.iacr.org/2025/239); the ZK-proof implementation itself lives in a separate library, [`polymesh-dart`](https://github.com/PolymeshAssociation/polymesh-dart), which this pallet calls into.
For the full model — commitments, curve-tree accumulators, nullifiers, auditors vs. mediators, the settlement/affirmation lifecycle, and anonymous fee payment — see the [Confidential Assets](/confidential-assets) docs. The rest of this page is a chain-level reference.
## Call surface
21 calls, grouped by the conceptual doc that covers them:
**Onboarding & accounts** — see [Onboarding & Accounts](/confidential-assets/onboarding-accounts)
- `register_accounts`, `register_encryption_keys`
**Asset creation & issuance** — see [Asset Operations](/confidential-assets/asset-operations)
- `create_asset`, `register_account_assets`, `mint_asset`
**Settlement** — see [Settlement Workflow](/confidential-assets/settlement-workflow)
- `create_settlement`, `sender_affirmation`, `receiver_affirmation`, `mediator_affirmation`, `sender_update_counter`, `sender_revert_affirmation`, `receiver_revert_affirmation`, `receiver_claim`, `batched_settlement`, `execute_instant_settlement`, `instant_sender_affirmation`, `instant_receiver_affirmation`
**Anonymous fees** — see [Anonymous Fees](/confidential-assets/anonymous-fees)
- `register_fee_accounts`, `topup_fee_accounts`, `submit_batched_proofs`, `relayer_submit_batched_proofs`
The pallet also defines 26 events and 46 errors — too many to usefully enumerate here; see `pallets/confidential-assets/src/lib.rs` in the Polymesh repository for the authoritative list.
## Chain-level facts
- **Fee account:** the pooled system account referenced in [Anonymous Fees](/confidential-assets/anonymous-fees) is a real, POLYX-backed account (`Currency = Balances` in the pallet's `Config`), derived deterministically from the pallet ID `pm/dartf`.
- **Curve-tree root update cadence is network-tuned.** On Testnet, asset/account curve-tree roots update at most every 10 minutes, with a maximum root age of 24 hours (assets) or 2 days (accounts/fee accounts). Develop uses much shorter intervals (18 seconds / 1–2 minutes) for fast local iteration. These are consensus-critical `Config` constants, not client-side settings.
- **Sizing limits** (`MaxSettlementLegs`, `MaxAssetAuditors`, `MaxAssetMediators`, `MaxAssetEncryptionKeys`, etc.) also differ slightly between Testnet and Develop — check the network you're targeting rather than assuming one set of limits.
## Availability checklist
1. Target **Testnet** — that's where Confidential Assets is publicly available going forward. The `Develop` runtime also includes the pallet for local node development, but the public Devnet deployment is being retired, so don't build against it as an ongoing target. The pallet will fail or be entirely absent on Mainnet.
2. Treat the interface as unstable pending the pending security audit; do not build production integrations against it yet.
3. Read the [Confidential Assets](/confidential-assets) docs before writing any code against this pallet — the account/settlement model is materially different from Polymesh's plaintext asset pallets and isn't self-explanatory from call signatures alone.
4. If you're upgrading a Devnet-based proof of concept to v8 Testnet, re-check network-specific constants (curve-tree root update intervals, sizing limits) rather than assuming Devnet values carry over.
---
## Other Runtime Changes Changelog - v7.4 to v8.0
This page covers v8 changes not significant enough for their own page, but still worth knowing about. It is part of the [v7.4 → v8.0 changelog](./010-overview.mdx).
**Audience:** anyone doing a thorough audit of chain-level changes; most integrators can skip this page unless a specific item below is relevant to them.
---
## Transaction fees split into two pallets
Polymesh's custom transaction-payment pallet (`pallets/transaction-payment`, crate `pallet-transaction-payment` at v7.4) is renamed to crate `polymesh-transaction-payment` and slimmed down. Chain v8 mounts it alongside the standard upstream `pallet_transaction_payment`:
- **`TransactionPayment`** (runtime index 6) — the standard upstream pallet: weight-to-fee conversion, the next-fee-multiplier adjustment, and the RPC fee-query helpers (`query_info`, `query_fee_details`, `compute_fee`, etc.) that used to live in Polymesh's own pallet.
- **`PolymeshTransactionPayment`** (runtime index 51) — what's left of the Polymesh-specific pallet: `current_payer`/`set_current_payer` (tracks who is paying for the current extrinsic, used by the subsidy system) and the `set_disable_fees` call.
If you called the RPC-style fee-query helpers directly against the old `TransactionPayment` pallet, they now live on the standard upstream pallet at the same pallet name but different index; `set_disable_fees` and payer-tracking move to `PolymeshTransactionPayment`.
The upstream `TransactionPayment` pallet also brings its own constant and storage: new constant `OperationalFeeMultiplier` (multiplies the priority "virtual tip" for `Operational`-class extrinsics), and new transient storage `TxPaymentCredit` (cleared every block; not user-facing state). Polymesh's old fee-curve constants `TransactionByteFee` and `WeightToFeeConst` are removed — fee-per-byte and the weight-to-fee curve are now configured through the upstream pallet's own `WeightToFee`/`LengthToFee` mechanism instead.
## `Sudo` moved to the upstream standard; gains `remove_key`
Same pattern as `Balances`/`Staking`: the local `pallets/sudo` crate is deleted, and `Sudo` is now the standard upstream `pallet-sudo`. `sudo`, `sudo_unchecked_weight`, `set_key`, and `sudo_as` are unchanged. New: `remove_key()`, which permanently and irreversibly clears the sudo key, and a corresponding `KeyRemoved` event.
## System: authorized runtime upgrades, and `ExtrinsicSuccess`/`ExtrinsicFailed` retyped
`frame_system` gains a two-step upgrade path: `authorize_upgrade(code_hash)` (or `authorize_upgrade_without_checks`) pre-authorizes a specific code hash, and anyone can subsequently call `apply_authorized_upgrade(code)` with the matching Wasm blob to perform the upgrade. New events `UpgradeAuthorized` and `RejectedInvalidAuthorizedUpgrade`; new errors `MultiBlockMigrationsOngoing`, `NothingAuthorized`, `Unauthorized`; new storage `AuthorizedUpgrade` (holds the pending authorization), `BlockSize`, `BlocksTillUpgrade`, `InherentsApplied`, and `ExtrinsicWeightReclaimed`; removed storage `AllExtrinsicsLen`. This is primarily relevant to governance/infrastructure tooling that performs runtime upgrades, not typical integrations.
More broadly relevant: `ExtrinsicSuccess { dispatch_info }` and `ExtrinsicFailed { dispatch_error, dispatch_info }` — emitted once per extrinsic in every block — change `dispatch_info`'s type from `DispatchInfo` to `DispatchEventInfo`. The fields (`weight`, `class`, `pays_fee`) are the same; the type name itself changes, which matters for typed decoders generated from chain metadata even though the underlying encoding is unaffected.
## Transaction extension: `CheckMetadataHash` (Merkleized metadata)
Chain v8 adds the `CheckMetadataHash` transaction extension (`frame_metadata_hash_extension::CheckMetadataHash`) to the runtime's signed-extension tuple. This implements the "Merkleized metadata" scheme (RFC-0078): the production runtime is built with a hash of its own metadata baked in, and a signer can opt to include that metadata hash in the signed payload.
This is the chain-level change that makes **generic offline signers and hardware wallets** able to decode and display full, human-readable Polymesh transaction details without a Polymesh-specific application — the device verifies the metadata it uses to render the transaction against the hash committed in what it signs. In practice this is what enables the generic Polkadot Ledger app (and similar tooling) to sign Polymesh transactions safely; see the [Ledger Hardware Wallet](/getting-started/keys/ledger) guide for the wallet-side view.
Because compatible signers decode against the runtime's own metadata rather than a fixed, hardcoded set of transaction formats, future runtime upgrades that add or change transaction types are typically understood and displayed correctly without requiring an app update — the metadata hash simply changes to match the new runtime.
Impact on transaction construction:
- The signed-extension list gained one entry, `CheckMetadataHash`, which contributes one byte (its mode: enabled/disabled) to the extra/additional-signed data. Anything that builds and signs extrinsics by manually assembling the signed extensions must account for it, in the correct position within the tuple, or signatures will be rejected.
- The extension supports two modes. With the mode **disabled**, only the mode byte is added and no metadata hash is required — existing signing flows continue to work with just the extra byte accounted for. With the mode **enabled**, the metadata hash is included, which is what unlocks the human-readable display on compatible signers.
- Up-to-date client libraries handle this automatically using the chain's current metadata (which carries the information needed to compute the hash); the Polymesh SDK and current Polkadot-JS tooling already account for it.
## Scheduler: retry management
New calls `set_retry`, `set_retry_named`, `cancel_retry`, `cancel_retry_named` let a scheduled task be configured to automatically retry (with a configurable period and retry count) if it fails. New events `RetrySet`, `RetryCancelled`, `RetryFailed`, `AgendaIncomplete`, backed by new `Retries` storage.
## Preimage: `ensure_updated`
New call `ensure_updated(hashes)` re-checks and updates the status of the given preimage hashes; the caller pays no fee if at least 90% are successfully updated. New storage `RequestStatusFor`; new errors `TooMany`, `TooFew`.
## Indices: `poke_deposit`
New call `poke_deposit(index)` re-syncs the deposit held for a short/vanity account index after a deposit-price change, refunding or charging the difference. New event `DepositPoked`. Low relevance unless you use short indices.
## New light-client pallets: `Beefy`, `Mmr`, `MmrLeaf`
Three new pallets support BEEFY (a secondary finality gadget for efficient light-client / bridge proofs) and its Merkle Mountain Range leaf data. This is what requires the new BEEFY session key covered in [Staking & Validators](./030-staking-and-validators.mdx#node-operators). Also new: `MultiBlockMigrations`, infrastructure for running storage migrations across multiple blocks rather than one.
## MultiSig: one error removed
`ChangeNotAllowed` is removed, with no replacement. No calls, events, or storage on `MultiSig` changed — this is the only difference.
## Removed: `Bridge` pallet (dead code)
The local `pallets/bridge` crate is deleted. It was already commented out of `construct_runtime!` at v7.4.0 (`//Bridge = 16,`) — it was source code that was never compiled into any runtime, so this has no practical impact.
## Migration checklist
1. If you query transaction fees via RPC-style calls, note they now resolve against the standard `TransactionPayment` pallet rather than Polymesh's custom one; `current_payer` and `set_disable_fees` move to `PolymeshTransactionPayment`. Drop any reliance on `TransactionByteFee`/`WeightToFeeConst`.
2. If your tooling calls `Sudo` calls by pallet name/index, re-check — the pallet moved to the upstream standard.
3. If you build or apply runtime upgrades programmatically, consider the new `authorize_upgrade`/`apply_authorized_upgrade` two-step path for large runtimes.
4. If you decode `ExtrinsicSuccess`/`ExtrinsicFailed` with generated types, regenerate them for the `DispatchInfo` → `DispatchEventInfo` rename.
5. If you construct and sign extrinsics by manually assembling signed extensions (rather than via the SDK or up-to-date Polkadot-JS), add the new `CheckMetadataHash` extension in the correct tuple position, or signatures will be rejected. Regenerating against the chain's current metadata handles this for you.
6. No action needed for `Bridge` — it was already inert.
For the complete literal list of every added/removed/modified call, event, error, storage item, and constant across every pallet, including the smaller ones not otherwise covered by this changelog series (`Babe`, `Grandpa`, `Historical`, `Offences`, `Permissions`, `Pips`, `ElectionProviderMultiPhase`, `AuthorityDiscovery`, `ImOnline`, committee-membership pallets, and `Base`), see the [Full Pallet API Reference](./110-pallet-api-reference.mdx).
---
## Full Pallet API Reference - v7.4 to v8.0
This page is a mechanically generated reference of every structural API change between Polymesh chain **v7.4** and **v8.0**, extracted directly from each release's runtime metadata (metadata v14, the only version available for both releases). The v8 side reflects runtime version **8.0.1**. It complements the narrative pages in the [v7.4 → v8.0 changelog](./010-overview.mdx) with a literal, pallet-by-pallet diff: every call, event, error, storage item, and constant that was added, removed, or had its argument/field shape change.
**Scope and method:**
- Comparison is by **name**, not by index. Call/event/error/storage indices are not stable across this upgrade (see the [overview](./010-overview.mdx)) and are ignored here entirely — an item that kept the same name and shape but moved to a different index is **not** listed as changed.
- "Modified" means the named item's argument names or underlying types changed. Cosmetic Rust-side renames introduced by the Polkadot SDK upgrade (for example `T::BlockNumber` becoming `BlockNumberFor`) are resolved to their underlying structural type before comparing, so they are **not** flagged as modified when nothing observable actually changed.
- For **constants**, only a type change counts as "modified" — constant _values_ (weight tables, fee coefficients, limits) are runtime tuning, not an API change, so value-only drift is not listed. The current value is still shown for reference, truncated if it's a long byte blob (e.g. a weight schedule).
- Pallets with no structural changes at all are omitted from the per-pallet detail below (they still appear as fully absent — nothing to migrate).
- The `v7.4` / `v8.0` columns show the Rust-level argument list or type signature, not the SCALE-encoded wire format.
## Pallets added
`Beefy`, `ConfidentialAssets` (Testnet only), `DidRegistrars`, `Mmr`, `MmrLeaf`, `MultiBlockMigrations`, `PolymeshTransactionPayment`, `Revive`, `Validators`
## Pallets removed
`CddServiceProviders`, `Contracts`, `PolymeshContracts`, `RandomnessCollectiveFlip`
The legacy Wasm/ink! contracts pallets `Contracts` (`pallet-contracts`) and `PolymeshContracts` (`polymesh_contracts`) are removed entirely; `pallet-revive` (listed under **Pallets added**) is the sole smart-contract pallet. See [Smart Contracts](./080-smart-contracts-evm.mdx).
Renamed pallets (for example `CddServiceProviders` → `DidRegistrars`) are not detected as renames by this name-based diff — they show up as one pallet removed and a different one added. See the [Identity & Onboarding](./060-identity-and-onboarding.mdx) page for the narrative mapping.
## Summary
| Pallet | Calls +/-/~ | Events +/-/~ | Errors +/-/~ | Storage +/-/~ | Constants +/-/~ |
| ------------------------------------------------------------- | ----------- | ------------ | ------------ | ------------- | --------------- |
| [Asset](#asset) | 1/0/3 | 2/0/2 | 7/2/0 | 1/0/0 | — |
| [AuthorityDiscovery](#authoritydiscovery) | — | — | — | 2/0/0 | — |
| [Babe](#babe) | 0/0/2 | — | — | — | 1/0/0 |
| [Balances](#balances) | 8/4/0 | 24/1/6 | 11/1/0 | 5/0/1 | 3/0/0 |
| [Base](#base) | — | 0/1/0 | — | — | — |
| [CommitteeMembership](#committeemembership) | — | 0/1/0 | — | — | — |
| [ElectionProviderMultiPhase](#electionprovidermultiphase) | 0/0/1 | — | 1/0/0 | 0/0/1 | 2/6/0 |
| [Grandpa](#grandpa) | — | — | — | 1/0/0 | 1/0/0 |
| [Historical](#historical) | — | 2/0/0 | — | 2/0/0 | — |
| [Identity](#identity) | 2/5/2 | 0/5/0 | 6/9/0 | 0/5/0 | — |
| [ImOnline](#imonline) | — | — | — | 0/0/1 | — |
| [Indices](#indices) | 1/0/0 | 1/0/0 | — | — | — |
| [MultiSig](#multisig) | — | — | 0/1/0 | — | — |
| [Nft](#nft) | 1/0/3 | 1/1/0 | 3/3/0 | 0/1/1 | — |
| [Offences](#offences) | — | — | — | 0/1/0 | — |
| [Permissions](#permissions) | — | — | — | 2/0/0 | — |
| [Pips](#pips) | 0/0/1 | — | 1/0/0 | — | — |
| [Portfolio](#portfolio) | 0/0/2 | — | 0/2/0 | 0/1/1 | — |
| [Preimage](#preimage) | 1/0/0 | — | 2/0/0 | 1/0/1 | — |
| [Relayer](#relayer) | 5/3/0 | 6/3/1 | 4/6/0 | 2/0/0 | — |
| [Scheduler](#scheduler) | 4/0/0 | 4/0/0 | — | 1/0/1 | — |
| [Session](#session) | — | 3/0/0 | — | 1/0/1 | 1/0/0 |
| [Settlement](#settlement) | 3/3/11 | 3/2/4 | 8/0/0 | 3/1/2 | 3/1/0 |
| [Staking](#staking) | 6/7/4 | 4/7/4 | 9/10/0 | 6/5/1 | 2/5/0 |
| [Sto](#sto) | 0/0/1 | — | 1/0/0 | — | — |
| [System](#system) | 3/0/0 | 2/0/2 | 3/0/0 | 5/1/1 | — |
| [TechnicalCommitteeMembership](#technicalcommitteemembership) | — | 0/1/0 | — | — | — |
| [TransactionPayment](#transactionpayment) | 0/1/0 | — | — | 1/0/0 | 1/2/0 |
| [UpgradeCommitteeMembership](#upgradecommitteemembership) | — | 0/1/0 | — | — | — |
| [Utility](#utility) | 0/1/0 | 0/1/0 | 0/3/0 | 0/1/0 | — |
## Pallet detail
### Asset
#### Calls
| Status | Name | v7.4 | v8.0 |
| -------- | --------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Added | `approve` | _(none)_ | `asset_id: AssetId, spender: T::AccountId, amount: Balance` |
| Modified | `controller_transfer` | `asset_id: AssetId, value: Balance, from_portfolio: PortfolioId` | `asset_id: AssetId, value: Balance, source: AssetHolder, destination_kind: AssetHolderKind` |
| Modified | `issue` | `asset_id: AssetId, amount: Balance, portfolio_kind: PortfolioKind` | `asset_id: AssetId, amount: Balance, asset_holder_kind: AssetHolderKind` |
| Modified | `redeem` | `asset_id: AssetId, value: Balance, portfolio_kind: PortfolioKind` | `asset_id: AssetId, value: Balance, asset_holder_kind: AssetHolderKind` |
#### Events
| Status | Name | v7.4 | v8.0 |
| -------- | --------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Added | `AllowanceSpent` | _(none)_ | `owner: T::AccountId, spender: T::AccountId, asset_id: AssetId, amount_spent: Balance, remaining_allowance: Balance` |
| Added | `Approval` | _(none)_ | `owner: T::AccountId, spender: T::AccountId, asset_id: AssetId, amount: Balance` |
| Modified | `AssetBalanceUpdated` | `IdentityId, AssetId, Balance, Option, Option, PortfolioUpdateReason` | `IdentityId, AssetId, Balance, Option, Option, HoldingsUpdateReason` |
| Modified | `ControllerTransfer` | `IdentityId, AssetId, PortfolioId, Balance` | `IdentityId, AssetId, AssetHolder, Balance` |
#### Errors
| Status | Name |
| ------- | ----------------------------------- |
| Added | `InsufficientAllowance` |
| Added | `InsufficientTokensLocked` |
| Added | `InvalidTransferInvalidReceiverDID` |
| Added | `KeyNotFoundForDid` |
| Added | `SelfOwnershipTransferNotAllowed` |
| Added | `UnauthorizedHolderKey` |
| Added | `WeightLimitExceeded` |
| Removed | `InvalidTransferInvalidReceiverCDD` |
| Removed | `InvalidTransferInvalidSenderCDD` |
#### Storage
| Status | Name | v7.4 | v8.0 |
| ------ | ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Added | `Allowances` | _(none)_ | `DoubleMap/NMap (Default) — hashers: Blake2_128Concat, Blake2_128Concat, Blake2_128Concat, key: (AccountId32, AccountId32, AssetId), value: U128` |
### AuthorityDiscovery
#### Storage
| Status | Name | v7.4 | v8.0 |
| ------ | ---------- | -------- | ------------------------------------------ |
| Added | `Keys` | _(none)_ | `Plain (Default) — WeakBoundedVec` |
| Added | `NextKeys` | _(none)_ | `Plain (Default) — WeakBoundedVec` |
### Babe
#### Calls
| Status | Name | v7.4 | v8.0 |
| -------- | ------------------------------ | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
| Modified | `report_equivocation` | `equivocation_proof: Box>, key_owner_proof: T::KeyOwnerProof` | `equivocation_proof: Box>>, key_owner_proof: T::KeyOwnerProof` |
| Modified | `report_equivocation_unsigned` | `equivocation_proof: Box>, key_owner_proof: T::KeyOwnerProof` | `equivocation_proof: Box>>, key_owner_proof: T::KeyOwnerProof` |
#### Constants
| Status | Name | v7.4 | v8.0 |
| ------ | --------------- | -------- | ------------ |
| Added | `MaxNominators` | _(none)_ | `U32 = 1024` |
### Balances
#### Calls
| Status | Name | v7.4 | v8.0 |
| ------- | -------------------------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------- |
| Added | `burn` | _(none)_ | `value: T::Balance, keep_alive: bool` |
| Added | `force_adjust_total_issuance` | _(none)_ | `direction: AdjustmentDirection, delta: T::Balance` |
| Added | `force_set_balance` | _(none)_ | `who: AccountIdLookupOf, new_free: T::Balance` |
| Added | `force_unreserve` | _(none)_ | `who: AccountIdLookupOf, amount: T::Balance` |
| Added | `transfer_all` | _(none)_ | `dest: AccountIdLookupOf, keep_alive: bool` |
| Added | `transfer_allow_death` | _(none)_ | `dest: AccountIdLookupOf, value: T::Balance` |
| Added | `transfer_keep_alive` | _(none)_ | `dest: AccountIdLookupOf, value: T::Balance` |
| Added | `upgrade_accounts` | _(none)_ | `who: Vec` |
| Removed | `burn_account_balance` | `amount: Balance` | _(none)_ |
| Removed | `deposit_block_reward_reserve_balance` | `value: Balance` | _(none)_ |
| Removed | `set_balance` | `who: ::Source, new_free: Balance, new_reserved: Balance` | _(none)_ |
| Removed | `transfer` | `dest: ::Source, value: Balance` | _(none)_ |
#### Events
| Status | Name | v7.4 | v8.0 |
| -------- | ---------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Added | `Burned` | _(none)_ | `who: T::AccountId, amount: T::Balance` |
| Added | `BurnedDebt` | _(none)_ | `amount: T::Balance` |
| Added | `BurnedHeld` | _(none)_ | `reason: T::RuntimeHoldReason, who: T::AccountId, amount: T::Balance` |
| Added | `Deposit` | _(none)_ | `who: T::AccountId, amount: T::Balance` |
| Added | `DustLost` | _(none)_ | `account: T::AccountId, amount: T::Balance` |
| Added | `Frozen` | _(none)_ | `who: T::AccountId, amount: T::Balance` |
| Added | `Held` | _(none)_ | `reason: T::RuntimeHoldReason, who: T::AccountId, amount: T::Balance` |
| Added | `Issued` | _(none)_ | `amount: T::Balance` |
| Added | `Locked` | _(none)_ | `who: T::AccountId, amount: T::Balance` |
| Added | `Minted` | _(none)_ | `who: T::AccountId, amount: T::Balance` |
| Added | `MintedCredit` | _(none)_ | `amount: T::Balance` |
| Added | `Released` | _(none)_ | `reason: T::RuntimeHoldReason, who: T::AccountId, amount: T::Balance` |
| Added | `Rescinded` | _(none)_ | `amount: T::Balance` |
| Added | `Restored` | _(none)_ | `who: T::AccountId, amount: T::Balance` |
| Added | `Slashed` | _(none)_ | `who: T::AccountId, amount: T::Balance` |
| Added | `Suspended` | _(none)_ | `who: T::AccountId, amount: T::Balance` |
| Added | `Thawed` | _(none)_ | `who: T::AccountId, amount: T::Balance` |
| Added | `TotalIssuanceForced` | _(none)_ | `old: T::Balance, new: T::Balance` |
| Added | `TransferAndHold` | _(none)_ | `reason: T::RuntimeHoldReason, source: T::AccountId, dest: T::AccountId, transferred: T::Balance` |
| Added | `TransferOnHold` | _(none)_ | `reason: T::RuntimeHoldReason, source: T::AccountId, dest: T::AccountId, amount: T::Balance` |
| Added | `Unexpected` | _(none)_ | `UnexpectedKind` |
| Added | `Unlocked` | _(none)_ | `who: T::AccountId, amount: T::Balance` |
| Added | `Upgraded` | _(none)_ | `who: T::AccountId` |
| Added | `Withdraw` | _(none)_ | `who: T::AccountId, amount: T::Balance` |
| Removed | `AccountBalanceBurned` | `IdentityId, T::AccountId, Balance` | _(none)_ |
| Modified | `BalanceSet` | `IdentityId, T::AccountId, Balance, Balance` | `who: T::AccountId, free: T::Balance` |
| Modified | `Endowed` | `Option, T::AccountId, Balance` | `account: T::AccountId, free_balance: T::Balance` |
| Modified | `ReserveRepatriated` | `T::AccountId, T::AccountId, Balance, Status` | `from: T::AccountId, to: T::AccountId, amount: T::Balance, destination_status: Status` |
| Modified | `Reserved` | `T::AccountId, Balance` | `who: T::AccountId, amount: T::Balance` |
| Modified | `Transfer` | `Option, T::AccountId, Option, T::AccountId, Balance, Option` | `from: T::AccountId, to: T::AccountId, amount: T::Balance` |
| Modified | `Unreserved` | `T::AccountId, Balance` | `who: T::AccountId, amount: T::Balance` |
#### Errors
| Status | Name |
| ------- | ------------------------- |
| Added | `DeadAccount` |
| Added | `DeltaZero` |
| Added | `ExistingVestingSchedule` |
| Added | `Expendability` |
| Added | `IssuanceDeactivated` |
| Added | `LockIdentifierNotFound` |
| Added | `MaxLocksExceeded` |
| Added | `TooManyFreezes` |
| Added | `TooManyHolds` |
| Added | `TooManyReserves` |
| Added | `VestingBalance` |
| Removed | `ReceiverCddMissing` |
#### Storage
| Status | Name | v7.4 | v8.0 |
| -------- | ------------------ | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Added | `Account` | _(none)_ | `Map (Default) — hashers: Blake2_128Concat, key: AccountId32, value: AccountData` |
| Added | `Freezes` | _(none)_ | `Map (Default) — hashers: Blake2_128Concat, key: AccountId32, value: BoundedVec>` |
| Added | `Holds` | _(none)_ | `Map (Default) — hashers: Blake2_128Concat, key: AccountId32, value: BoundedVec>` |
| Added | `InactiveIssuance` | _(none)_ | `Plain (Default) — U128` |
| Added | `Reserves` | _(none)_ | `Map (Default) — hashers: Blake2_128Concat, key: AccountId32, value: BoundedVec>` |
| Modified | `Locks` | `Map (Default) — hashers: Blake2_128Concat, key: AccountId32, value: Vec>` | `Map (Default) — hashers: Blake2_128Concat, key: AccountId32, value: WeakBoundedVec>` |
#### Constants
| Status | Name | v7.4 | v8.0 |
| ------ | ------------- | -------- | ---------- |
| Added | `MaxFreezes` | _(none)_ | `U32 = 0` |
| Added | `MaxLocks` | _(none)_ | `U32 = 50` |
| Added | `MaxReserves` | _(none)_ | `U32 = 50` |
### Base
#### Events
| Status | Name | v7.4 | v8.0 |
| ------- | ----------------- | ----------------------- | -------- |
| Removed | `UnexpectedError` | `Option` | _(none)_ |
### CommitteeMembership
#### Events
| Status | Name | v7.4 | v8.0 |
| ------- | ------- | -------- | -------- |
| Removed | `Dummy` | _(none)_ | _(none)_ |
### ElectionProviderMultiPhase
#### Calls
| Status | Name | v7.4 | v8.0 |
| -------- | --------------------- | --------------------------------------------------------------- | -------- |
| Modified | `governance_fallback` | `maybe_max_voters: Option, maybe_max_targets: Option` | _(none)_ |
#### Errors
| Status | Name |
| ------ | --------------------------- |
| Added | `PreDispatchDifferentRound` |
#### Storage
| Status | Name | v7.4 | v8.0 |
| -------- | ---------- | ---------------------------------- | -------------------------------------------------------------------------------------------- |
| Modified | `Snapshot` | `Plain (Optional) — RoundSnapshot` | `Plain (Optional) — RoundSnapshot)>` |
#### Constants
| Status | Name | v7.4 | v8.0 |
| ------- | ------------------------- | ----------------- | ------------- |
| Added | `MaxBackersPerWinner` | _(none)_ | `U32 = 40000` |
| Added | `MinerMaxWinners` | _(none)_ | `U32 = 1000` |
| Removed | `BetterUnsignedThreshold` | `Perbill = 0.01%` | _(none)_ |
| Removed | `MaxElectableTargets` | `U16 = 10000` | _(none)_ |
| Removed | `MaxElectingVoters` | `U32 = 40000` | _(none)_ |
| Removed | `SignedDepositBase` | `U128 = 0` | _(none)_ |
| Removed | `SignedPhase` | `U32 = 0` | _(none)_ |
| Removed | `UnsignedPhase` | `U32 = 150` | _(none)_ |
### Grandpa
#### Storage
| Status | Name | v7.4 | v8.0 |
| ------ | ------------- | -------- | ------------------------------------------------- |
| Added | `Authorities` | _(none)_ | `Plain (Default) — WeakBoundedVec<(Public, U64)>` |
#### Constants
| Status | Name | v7.4 | v8.0 |
| ------ | --------------- | -------- | ------------ |
| Added | `MaxNominators` | _(none)_ | `U32 = 1024` |
### Historical
#### Events
| Status | Name | v7.4 | v8.0 |
| ------ | ------------- | -------- | --------------------- |
| Added | `RootStored` | _(none)_ | `index: SessionIndex` |
| Added | `RootsPruned` | _(none)_ | `up_to: SessionIndex` |
#### Storage
| Status | Name | v7.4 | v8.0 |
| ------ | -------------------- | -------- | ---------------------------------------------------------------------- |
| Added | `HistoricalSessions` | _(none)_ | `Map (Optional) — hashers: Twox64Concat, key: U32, value: (H256, U32)` |
| Added | `StoredRange` | _(none)_ | `Plain (Optional) — (U32, U32)` |
### Identity
#### Calls
| Status | Name | v7.4 | v8.0 |
| -------- | ---------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------ |
| Added | `register_did` | _(none)_ | `target_account: T::AccountId` |
| Added | `self_register_did` | _(none)_ | _(none)_ |
| Removed | `change_cdd_requirement_for_mk_rotation` | `auth_required: bool` | _(none)_ |
| Removed | `create_child_identities` | `child_keys: Vec>, expires_at: T::Moment` | _(none)_ |
| Removed | `create_child_identity` | `secondary_key: T::AccountId` | _(none)_ |
| Removed | `invalidate_cdd_claims` | `cdd: IdentityId, disable_from: T::Moment, expiry: Option` | _(none)_ |
| Removed | `unlink_child_identity` | `child_did: IdentityId` | _(none)_ |
| Modified | `accept_primary_key` | `rotation_auth_id: u64, optional_cdd_auth_id: Option` | `rotation_auth_id: u64` |
| Modified | `rotate_primary_key_to_secondary` | `auth_id: u64, optional_cdd_auth_id: Option` | `auth_id: u64` |
#### Events
| Status | Name | v7.4 | v8.0 |
| ------- | ------------------------------------ | -------------------------------------- | -------- |
| Removed | `AssetDidRegistered` | `IdentityId, Ticker` | _(none)_ |
| Removed | `CddClaimsInvalidated` | `IdentityId, T::Moment` | _(none)_ |
| Removed | `CddRequirementForPrimaryKeyUpdated` | `bool` | _(none)_ |
| Removed | `ChildDidCreated` | `IdentityId, IdentityId, T::AccountId` | _(none)_ |
| Removed | `ChildDidUnlinked` | `IdentityId, IdentityId, IdentityId` | _(none)_ |
#### Errors
| Status | Name |
| ------- | -------------------------------------------- |
| Added | `IdentityNotFoundForAccountPortfolio` |
| Added | `InvalidAuthorizationFromDidRegistrar` |
| Added | `NotDidRegistrarAttestation` |
| Added | `TargetDidInactive` |
| Added | `UnAuthorizedDidRegistrar` |
| Added | `UnauthorizedCallerDidInactive` |
| Removed | `AccountBasedPortfoliosCannotHaveCustodians` |
| Removed | `InvalidAuthorizationFromCddProvider` |
| Removed | `IsChildIdentity` |
| Removed | `NoParentIdentity` |
| Removed | `NotCddProviderAttestation` |
| Removed | `NotParentOrChildIdentity` |
| Removed | `TargetHasNoCdd` |
| Removed | `UnAuthorizedCddProvider` |
| Removed | `UnauthorizedCallerDidMissingCdd` |
#### Storage
| Status | Name | v7.4 | v8.0 |
| ------- | ------------------------------ | ---------------------------------------------------------------------------------------------------- | -------- |
| Removed | `CddAuthForPrimaryKeyRotation` | `Plain (Default) — Bool` | _(none)_ |
| Removed | `ChildDid` | `DoubleMap/NMap (Default) — hashers: Identity, Identity, key: (IdentityId, IdentityId), value: Bool` | _(none)_ |
| Removed | `CurrentPayer` | `Plain (Optional) — AccountId32` | _(none)_ |
| Removed | `ParentDid` | `Map (Optional) — hashers: Identity, key: IdentityId, value: IdentityId` | _(none)_ |
| Removed | `StorageVersion` | `Plain (Default) — Version` | _(none)_ |
### ImOnline
#### Storage
| Status | Name | v7.4 | v8.0 |
| -------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Modified | `ReceivedHeartbeats` | `DoubleMap/NMap (Optional) — hashers: Twox64Concat, Twox64Concat, key: (U32, U32), value: WrapperOpaque` | `DoubleMap/NMap (Optional) — hashers: Twox64Concat, Twox64Concat, key: (U32, U32), value: Bool` |
### Indices
#### Calls
| Status | Name | v7.4 | v8.0 |
| ------ | -------------- | -------- | ------------------------ |
| Added | `poke_deposit` | _(none)_ | `index: T::AccountIndex` |
#### Events
| Status | Name | v7.4 | v8.0 |
| ------ | -------------- | -------- | ------------------------------------------------------------------------------------------------- |
| Added | `DepositPoked` | _(none)_ | `who: T::AccountId, index: T::AccountIndex, old_deposit: BalanceOf, new_deposit: BalanceOf` |
### MultiSig
#### Errors
| Status | Name |
| ------- | ------------------ |
| Removed | `ChangeNotAllowed` |
### Nft
#### Calls
| Status | Name | v7.4 | v8.0 |
| -------- | --------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| Added | `transfer_nft` | _(none)_ | `nfts: NFTs, to: T::AccountId, memo: Option` |
| Modified | `controller_transfer` | `nfts: NFTs, source_portfolio: PortfolioId, callers_portfolio_kind: PortfolioKind` | `nfts: NFTs, source: AssetHolder, destination_kind: AssetHolderKind` |
| Modified | `issue_nft` | `asset_id: AssetId, nft_metadata_attributes: Vec, portfolio_kind: PortfolioKind` | `asset_id: AssetId, nft_metadata_attributes: Vec, holdings_kind: AssetHolderKind` |
| Modified | `redeem_nft` | `asset_id: AssetId, nft_id: NFTId, portfolio_kind: PortfolioKind, number_of_keys: Option` | `asset_id: AssetId, nft_id: NFTId, holdings_kind: AssetHolderKind, number_of_keys: Option` |
#### Events
| Status | Name | v7.4 | v8.0 |
| ------- | --------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| Added | `NFTHoldingsUpdated` | _(none)_ | `IdentityId, NFTs, Option, Option, HoldingsUpdateReason` |
| Removed | `NFTPortfolioUpdated` | `IdentityId, NFTs, Option, Option, PortfolioUpdateReason` | _(none)_ |
#### Errors
| Status | Name |
| ------- | ----------------------------------------------- |
| Added | `InvalidNFTTransferInvalidReceiverDID` |
| Added | `InvalidNFTTransferSenderDidMatchesReceiverDid` |
| Added | `NFTIsNotLocked` |
| Removed | `InvalidNFTTransferInvalidReceiverCDD` |
| Removed | `InvalidNFTTransferInvalidSenderCDD` |
| Removed | `InvalidNFTTransferSenderIdMatchesReceiverId` |
#### Storage
| Status | Name | v7.4 | v8.0 |
| -------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Removed | `NFTOwner` | `DoubleMap/NMap (Optional) — hashers: Blake2_128Concat, Blake2_128Concat, key: (AssetId, NFTId), value: PortfolioId` | _(none)_ |
| Modified | `NFTHolder` | `DoubleMap/NMap (Default) — hashers: Twox64Concat, Blake2_128Concat, key: (AccountId32, (AssetId, NFTId)), value: NFTOwnerStatus` | `DoubleMap/NMap (Default) — hashers: Twox64Concat, Blake2_128Concat, Blake2_128Concat, key: (AccountId32, AssetId, NFTId), value: NFTOwnerStatus` |
### Offences
#### Storage
| Status | Name | v7.4 | v8.0 |
| ------- | -------------------- | ---------------------------------------------------------------------- | -------- |
| Removed | `ReportsByKindIndex` | `Map (Default) — hashers: Twox64Concat, key: [U8; 16], value: Vec` | _(none)_ |
### Permissions
#### Storage
| Status | Name | v7.4 | v8.0 |
| ------ | ------------------------- | -------- | --------------------------------- |
| Added | `CurrentDispatchableName` | _(none)_ | `Plain (Default) — ExtrinsicName` |
| Added | `CurrentPalletName` | _(none)_ | `Plain (Default) — PalletName` |
### Pips
#### Calls
| Status | Name | v7.4 | v8.0 |
| -------- | ---------- | -------- | ------------ |
| Modified | `snapshot` | _(none)_ | `limit: u32` |
#### Errors
| Status | Name |
| ------ | ----------------------- |
| Added | `SnapshotLimitTooSmall` |
### Portfolio
#### Calls
| Status | Name | v7.4 | v8.0 |
| -------- | ------------------ | ---------------------------------------------- | ---------------------------------------------------------------------- |
| Modified | `delete_portfolio` | `num: PortfolioNumber` | `portfolio_number: PortfolioNumber` |
| Modified | `rename_portfolio` | `num: PortfolioNumber, to_name: PortfolioName` | `portfolio_number: PortfolioNumber, new_portfolio_name: PortfolioName` |
#### Errors
| Status | Name |
| ------- | -------------------------------------------- |
| Removed | `AccountBasedPortfoliosCannotHaveCustodians` |
| Removed | `InvalidTransferSenderIdMatchesReceiverId` |
#### Storage
| Status | Name | v7.4 | v8.0 |
| -------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Removed | `StorageVersion` | `Plain (Default) — Version` | _(none)_ |
| Modified | `PortfolioNFT` | `DoubleMap/NMap (Default) — hashers: Twox64Concat, Blake2_128Concat, key: (PortfolioId, (AssetId, NFTId)), value: Bool` | `DoubleMap/NMap (Default) — hashers: Twox64Concat, Blake2_128Concat, Blake2_128Concat, key: (PortfolioId, AssetId, NFTId), value: Bool` |
### Preimage
#### Calls
| Status | Name | v7.4 | v8.0 |
| ------ | ---------------- | -------- | ---------------------- |
| Added | `ensure_updated` | _(none)_ | `hashes: Vec` |
#### Errors
| Status | Name |
| ------ | --------- |
| Added | `TooFew` |
| Added | `TooMany` |
#### Storage
| Status | Name | v7.4 | v8.0 |
| -------- | ------------------ | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Added | `RequestStatusFor` | _(none)_ | `Map (Optional) — hashers: Identity, key: H256, value: RequestStatus` |
| Modified | `StatusFor` | `Map (Optional) — hashers: Identity, key: H256, value: RequestStatus` | `Map (Optional) — hashers: Identity, key: H256, value: OldRequestStatus` |
### Relayer
#### Calls
| Status | Name | v7.4 | v8.0 |
| ------- | ------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Added | `accept_subsidy` | _(none)_ | `paying_key: T::AccountId` |
| Added | `approve_subsidy` | _(none)_ | `user_key: T::AccountId, polyx_limit: Balance` |
| Added | `relay_tx` | _(none)_ | `target: T::AccountId, signature: T::OffChainSignature, call: Box<::RuntimeCall>, expires_at: T::Moment` |
| Added | `remove_subsidy` | _(none)_ | `user_key: T::AccountId, paying_key: T::AccountId` |
| Added | `revoke_subsidy` | _(none)_ | `user_key: T::AccountId` |
| Removed | `accept_paying_key` | `auth_id: u64` | _(none)_ |
| Removed | `remove_paying_key` | `user_key: T::AccountId, paying_key: T::AccountId` | _(none)_ |
| Removed | `set_paying_key` | `user_key: T::AccountId, polyx_limit: Balance` | _(none)_ |
#### Events
| Status | Name | v7.4 | v8.0 |
| -------- | ----------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Added | `AcceptedSubsidy` | _(none)_ | `user_key: T::AccountId, paying_key: T::AccountId, initial_polyx_limit: Balance` |
| Added | `ApprovedSubsidy` | _(none)_ | `user_key: T::AccountId, paying_key: T::AccountId, initial_polyx_limit: Balance` |
| Added | `RelayedTx` | _(none)_ | `caller: T::AccountId, target: T::AccountId, result: DispatchResult` |
| Added | `RemovedPendingSubsidy` | _(none)_ | `user_key: T::AccountId, paying_key: T::AccountId, initial_polyx_limit: Balance` |
| Added | `RemovedSubsidy` | _(none)_ | `user_key: T::AccountId, paying_key: T::AccountId, remaining: Balance` |
| Added | `SubsidyDebited` | _(none)_ | `user_key: T::AccountId, paying_key: T::AccountId, amount: Balance` |
| Removed | `AcceptedPayingKey` | `EventDid, T::AccountId, T::AccountId` | _(none)_ |
| Removed | `AuthorizedPayingKey` | `EventDid, T::AccountId, T::AccountId, Balance, u64` | _(none)_ |
| Removed | `RemovedPayingKey` | `EventDid, T::AccountId, T::AccountId` | _(none)_ |
| Modified | `UpdatedPolyxLimit` | `EventDid, T::AccountId, T::AccountId, Balance, Balance` | `user_key: T::AccountId, paying_key: T::AccountId, remaining: Balance, old_remaining: Balance` |
#### Errors
| Status | Name |
| ------- | --------------------------- |
| Added | `ExpiredRelayTx` |
| Added | `InvalidSignature` |
| Added | `NoPendingSubsidy` |
| Added | `NotAuthorized` |
| Removed | `BadAuthorizationType` |
| Removed | `IdentityNotFound` |
| Removed | `NotAuthorizedForPayingKey` |
| Removed | `NotAuthorizedForUserKey` |
| Removed | `PayingKeyCddMissing` |
| Removed | `UserKeyCddMissing` |
#### Storage
| Status | Name | v7.4 | v8.0 |
| ------ | ------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------- |
| Added | `PendingSubsidies` | _(none)_ | `DoubleMap/NMap (Optional) — hashers: Blake2_128Concat, Blake2_128Concat, key: (AccountId32, AccountId32), value: U128` |
| Added | `RelayTxNonces` | _(none)_ | `Map (Default) — hashers: Twox64Concat, key: AccountId32, value: U64` |
### Scheduler
#### Calls
| Status | Name | v7.4 | v8.0 |
| ------ | -------------------- | -------- | ------------------------------------------------------------------------------ |
| Added | `cancel_retry` | _(none)_ | `task: TaskAddress>` |
| Added | `cancel_retry_named` | _(none)_ | `id: TaskName` |
| Added | `set_retry` | _(none)_ | `task: TaskAddress>, retries: u8, period: BlockNumberFor` |
| Added | `set_retry_named` | _(none)_ | `id: TaskName, retries: u8, period: BlockNumberFor` |
#### Events
| Status | Name | v7.4 | v8.0 |
| ------ | ------------------ | -------- | ---------------------------------------------------------------------------------------------------- |
| Added | `AgendaIncomplete` | _(none)_ | `when: BlockNumberFor` |
| Added | `RetryCancelled` | _(none)_ | `task: TaskAddress>, id: Option` |
| Added | `RetryFailed` | _(none)_ | `task: TaskAddress>, id: Option` |
| Added | `RetrySet` | _(none)_ | `task: TaskAddress>, id: Option, period: BlockNumberFor, retries: u8` |
#### Storage
| Status | Name | v7.4 | v8.0 |
| -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Added | `Retries` | _(none)_ | `Map (Optional) — hashers: Blake2_128Concat, key: (U32, U32), value: RetryConfig` |
| Modified | `Agenda` | `Map (Default) — hashers: Twox64Concat, key: U32, value: BoundedVec