v1.1 — Holder asset protection
| Field | Value |
|---|---|
| Upgrade plan name | v1.1 |
| Upstream commit | f425a0d — feat(tokenfactory): add single-leg pull and burn authorizations |
| Consensus effect | Yes. Message behaviour changes, and tokenfactory moves from consensus version 2 to 3. |
| Store upgrade | Empty. No store is added, renamed, or deleted. |
| Applies to | Any network that submits a v1.1 software-upgrade plan. No production network exists yet. |
v1.1 does two things under one plan name:
- Holder asset protection. No role can burn or move a holder's token or NFT without the holder's own signature. This part is a behaviour change only: no new state, no new message, no protobuf change, no migration.
- Single-leg authorizations. The two-phase authorization grows beyond swap:
the holder can now grant a pull (
kind=PULL) or a burn (kind=BURN), settled by one counterparty message. This part adds a protobuf field and two messages, and movestokenfactoryto consensus version 3 with a migration.
1. Behaviour changes
Holder asset protection
The changes are the whole subject of Holder asset protection; in short:
| Message | Before | v1.1 |
|---|---|---|
MsgForceTransfer | Worked for the denom admin | Always rejected — ErrHolderAssetProtection (1118) |
MsgBurn from another address | Worked for the denom admin | Rejected — ErrHolderAssetProtection (1118) |
MsgBurn of the sender's own balance | Worked | Unchanged |
MsgBurnNFT by the class issuer or module authority | Worked | Rejected — ErrHolderAssetProtection (1214) |
MsgBurnNFT by the token's owner | Worked | Unchanged |
MsgMint | Worked | Unchanged — adds supply; touches no existing holder |
| Blocklist / freeze | Worked | Unchanged — stops sends; moves nothing |
The latent internal forceTransfer helper was removed from the tokenfactory
keeper, closing a bypass that no message reached but that a future caller could
have.
Authorization kinds
MsgCreateAuthorization gains a kind field that binds the authorization to the
one purpose it may be spent on:
| Kind | Value | Consuming message | What it does |
|---|---|---|---|
AUTHORIZATION_KIND_SWAP | 1 | MsgExecuteSwap | The existing two-leg swap. |
AUTHORIZATION_KIND_PULL | 2 | MsgExecutePull | Single-leg pull: moves the authorized input to the executor. |
AUTHORIZATION_KIND_BURN | 3 | MsgExecuteBurn | Single-leg burn: the executor burns the authorized input. |
AUTHORIZATION_KIND_UNSPECIFIED (0) is not accepted for a new authorization;
MsgCreateAuthorization.ValidateBasic requires an explicit kind. A settlement
message re-checks the kind, so a PULL authorization cannot be consumed by
MsgExecuteSwap, and vice versa.
A single-leg authorization has no output leg and a zero gas fee: the
holder names an input, a ceiling, and an executor, and nothing is minted back.
An NFT input pins the ceiling to 1.
2. The settlement messages
Both new messages share the shape executor, holder, order_seq, chain_id,
amount, and both are signed by the executor. They are served by the
SwapMsg gRPC service — the same service that already carries
MsgExecuteSwap and MsgRevokeAuthorization — not by the module's Msg
service.
| Message | Settles | Requires | Effect on success |
|---|---|---|---|
MsgExecutePull | kind=PULL | The executor matches the authorization; the amount is within the ceiling; the holder and executor pass the blacklist and restriction checks. | Releases the reservation, transfers the amount from the holder to the executor, marks the authorization consumed. Nothing is burned; supply is unchanged. |
MsgExecuteBurn | kind=BURN | The executor matches; the amount is within the ceiling; for fungible input the executor is the denom admin, for NFT input the executor is the class issuer; the blacklist and restriction checks pass. | Releases the reservation, moves the amount from the holder to the module and burns it (supply shrinks), marks the authorization consumed. |
Both release the entire max_from reservation, so any budget the settlement did
not spend becomes spendable by the holder again. Both settle inside a cached
context and roll back whole: a failed settlement leaves the authorization, the
reservation, and the balances untouched.
A pull or burn on an NFT unlocks the escrowed token and moves or burns it through the internal, consent-gated swap path. A pull needs no class-issuer role for the executor; a burn does.
ErrHolderAssetProtection is not raised by either message: both act inside a
holder-signed authorization, which is exactly the consent the rule requires.
3. Cross-module effects
| Area | Effect |
|---|---|
| NFT market | A class issuer can no longer burn a listed holder token. When the holder burns their own token, the market's burn hook clears the listing and refunds the seller's deposit. |
| Income | Releasing an NFT's claimable now depends on the holder burning their own token; the issuer's burn path is gone. |
| Swap legs | Unchanged. BurnForSwap and the internal raw transfer still run under the holder's signed swap authorization. |
| Liquidity | Unchanged. The module-internal LP-share burn is not a holder balance and is not gated by the rule. |
4. Technical scope
Changed behaviour
x/tokenfactory/keeper/msg_server.go— theBurnandForceTransferguards.x/tokenfactory/keeper/bankactions.go— the unusedforceTransferhelper removed.x/nft/keeper/mint.go— theBurnNFTownership guard.
Added for single-leg authorizations
proto/centerchain/tokenfactory/v1/swap.proto— theAuthorizationKindenum, thekindfield (12) onMsgCreateAuthorization, and theMsgExecutePullandMsgExecuteBurnmessages on theSwapMsgservice.x/tokenfactory/keeper/swap.go— theExecutePullandExecuteBurnhandlers and their NFT helpers.x/tokenfactory/types/events.go—pull_executedandburn_executed.x/tokenfactory/types/errors.go—ErrHolderAssetProtection(1118).x/nft/types/errors.go—ErrHolderAssetProtection(1214).
Consensus version and migration
tokenfactory moves from consensus version 2 to 3. The migration
MigrateSwapAuthorizationKindV3 rewrites every pre-existing authorization's
UNSPECIFIED kind to SWAP, so an authorization created before the upgrade
keeps meaning what it meant. A BURN authorization never existed before v1.1,
so the migration creates none. The Authorization state schema is unchanged, so
ConsensusVersion moves to 3 once, for the kind binding, and stays 3 across the
pull and burn additions.
Because the chain application runs module migrations through its upgrade
handler, the v1.1 plan is what applies this migration.
5. Upgrading
The plan name is v1.1 and the store upgrade is empty. Applying it is a governed
software upgrade, not a restart: see Upgrading a network.
The release is not production-ready. There is no production network, and the public testnet runs four validators on a single host.
6. Verifying the result
After the height:
- Block production continues.
MsgForceTransferis rejected withcode 1118.- The denom admin cannot burn a holder's balance; the holder can burn their own.
- The class issuer cannot burn a holder's NFT; the owner can burn their own.
- A
MsgCreateAuthorizationwithout an explicit kind is rejected. MsgExecutePullmoves the authorized amount to the executor without changing supply;MsgExecuteBurnrequires the denom admin (or class issuer) and shrinks supply.
The generated gRPC contract for these messages is in the swap proto reference, regenerated from the v1.1 pin.