Holder asset protection
A token or NFT on CenterChain cannot be burned or moved out of a holder's account by anyone but the holder. Holding an administrative role — the denom admin, the class issuer, or the module authority — does not let that role act on somebody else's balance or token.
This is the rule introduced in v1.1. Before it, the denom admin could burn from another address and force-transfer a balance, and a class issuer could burn any token in its class. Those paths are closed.
What the rule covers
| Message | Before | Now |
|---|---|---|
MsgForceTransfer | The denom admin could move a balance to another address | Always rejected, with ErrHolderAssetProtection (1118). The message stays in the API so a caller gets a stable error instead of "unimplemented". |
MsgBurn from another address (burn_from_address ≠ sender) | The denom admin could burn any holder's balance | Rejected, with ErrHolderAssetProtection (1118). |
MsgBurn of the sender's own balance | Worked | Unchanged. A denom admin can still burn what it holds itself. |
MsgBurnNFT by the class issuer or the module authority | Worked | Rejected, with ErrHolderAssetProtection (1214). |
MsgBurnNFT by the token's owner | Worked | Unchanged. The owner can burn their own token. |
The check is a direct comparison inside the message handler: the burn is allowed only when the address acting is the address that holds the asset. There is no capability, no role, and no module-authority exception that re-opens the path.
The two error codes
The same rule is enforced in two modules, so it has one error code per module:
| Module | Code | Identifier |
|---|---|---|
| Token factory | 1118 | ErrHolderAssetProtection |
| NFT | 1214 | ErrHolderAssetProtection |
Both carry the message holder asset protection: the operation requires the holder's own consent. The codes are distinct because each module registers its
own error space; match on the wrapped sentinel, not on the number, across
modules.
What an issuer can still do
Holder protection removes the powers that touch existing holder assets. It does not touch the powers that create supply, change policy, or gate who may receive:
| Still allowed | Effect |
|---|---|
MsgMint | Adds new supply. The issuer's own balance grows; no existing holder is affected. |
Denom restriction, delegates, MsgChangeAdmin | Change who may receive the denom of a restricted denom. Circulation control, not seizure. |
| Blocklist / freeze | An entry stops a locked address from sending or receiving the denom. The balance stays where it is; nothing changes hands. |
Class metadata, MsgChangeClassIssuer | Class administration. Metadata rules still apply and immutability still binds. |
MsgBurnNFT of a token the signer owns | The signer is the holder, so the rule does not bind. |
The compliance tool that remains is blocking, not seizure: an address is locked out of the denom and its assets stay put. The chain no longer offers a way to take an asset away from a holder without that holder's signature.
Where holder consent still authorizes a move
Holder protection does not forbid a holder from moving their own asset. It requires that the holder signs. Two paths make that explicit:
- Single-leg authorizations.
MsgExecutePullandMsgExecuteBurnsettle against aMsgCreateAuthorizationthe holder signed, so the move is inside a permission the holder granted. See Two-phase execution. - Swap settlement. A swap leg moves an NFT or burns fungible input under the holder's signed authorization, as before.
Both are holder-signed. The rule bars acting without the holder's signature; it does not bar the holder from acting.
Module-internal paths
One burn path is not a holder asset at all: the liquidity
module burns LP shares when a position is removed. Those shares are administered
by the liquidity module account, and the burn is an internal keeper call rather
than MsgBurn, so the holder-protection guard does not apply. It is a different
thing from burning a holder's balance.
Why the old switches are inert
The chain still declares two capabilities that used to gate the removed powers:
| Capability | Used to gate | Now |
|---|---|---|
enable_force_transfer | MsgForceTransfer | Inert. The message is rejected whether or not the capability is set. |
enable_burn_from | Burning from an address other than the sender's | Inert. The burn-from path is rejected whether or not the capability is set. |
A capability answers whether an action exists on this chain. Holder protection answers a stronger question: the action does not exist for another holder's asset at all, so the switch has nothing left to enable. See Security and governance.