Skip to main content
Version: Next

v1.1 — Holder asset protection

FieldValue
Upgrade plan namev1.1
Upstream commitf425a0d — feat(tokenfactory): add single-leg pull and burn authorizations
Consensus effectYes. Message behaviour changes, and tokenfactory moves from consensus version 2 to 3.
Store upgradeEmpty. No store is added, renamed, or deleted.
Applies toAny network that submits a v1.1 software-upgrade plan. No production network exists yet.

v1.1 does two things under one plan name:

  1. 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.
  2. 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 moves tokenfactory to consensus version 3 with a migration.

1. Behaviour changes​

Holder asset protection​

The changes are the whole subject of Holder asset protection; in short:

MessageBeforev1.1
MsgForceTransferWorked for the denom adminAlways rejected — ErrHolderAssetProtection (1118)
MsgBurn from another addressWorked for the denom adminRejected — ErrHolderAssetProtection (1118)
MsgBurn of the sender's own balanceWorkedUnchanged
MsgBurnNFT by the class issuer or module authorityWorkedRejected — ErrHolderAssetProtection (1214)
MsgBurnNFT by the token's ownerWorkedUnchanged
MsgMintWorkedUnchanged — adds supply; touches no existing holder
Blocklist / freezeWorkedUnchanged — 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:

KindValueConsuming messageWhat it does
AUTHORIZATION_KIND_SWAP1MsgExecuteSwapThe existing two-leg swap.
AUTHORIZATION_KIND_PULL2MsgExecutePullSingle-leg pull: moves the authorized input to the executor.
AUTHORIZATION_KIND_BURN3MsgExecuteBurnSingle-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.

MessageSettlesRequiresEffect on success
MsgExecutePullkind=PULLThe 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.
MsgExecuteBurnkind=BURNThe 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​

AreaEffect
NFT marketA 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.
IncomeReleasing an NFT's claimable now depends on the holder burning their own token; the issuer's burn path is gone.
Swap legsUnchanged. BurnForSwap and the internal raw transfer still run under the holder's signed swap authorization.
LiquidityUnchanged. 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 — the Burn and ForceTransfer guards.
  • x/tokenfactory/keeper/bankactions.go — the unused forceTransfer helper removed.
  • x/nft/keeper/mint.go — the BurnNFT ownership guard.

Added for single-leg authorizations

  • proto/centerchain/tokenfactory/v1/swap.proto — the AuthorizationKind enum, the kind field (12) on MsgCreateAuthorization, and the MsgExecutePull and MsgExecuteBurn messages on the SwapMsg service.
  • x/tokenfactory/keeper/swap.go — the ExecutePull and ExecuteBurn handlers and their NFT helpers.
  • x/tokenfactory/types/events.go — pull_executed and burn_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.
  • MsgForceTransfer is rejected with code 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 MsgCreateAuthorization without an explicit kind is rejected.
  • MsgExecutePull moves the authorized amount to the executor without changing supply; MsgExecuteBurn requires 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.