Skip to main content
Version: Next

v1.2 — Claim permits and the authorization rework

FieldValue
Upgrade plan nameNone. v1.2 is applied by a genesis reset with a new chain ID. No v1.2 software-upgrade plan is registered.
Upstream commit6a8a6e0 — fix(tokenfactory): use executor as action permit signer in execute mint and send
Consensus effectYes. The authorization state schema, the message surface, the ante handler, and the gRPC service layout change.
Store upgradeNone. A network starts from a v1.2 genesis; there is no migration from v1.1 state.
Applies tocenterchain-testnet-2, and any network that starts from a v1.2 genesis. There is no production network.

v1.2 changes how a holder authorizes an action, and adds a second, single transaction route for the two actions that do not need a settlement step.

  1. Claim permits. A signer can authorize a mint, a send, an NFT mint, or an NFT send with a signature alone. An executor submits the signed permit in one transaction and pays the network fee. The signer never submits a transaction and never needs ucen.
  2. The authorization rework. A two-phase authorization now has a block-based expiry, no longer reserves the holder's balance or locks an NFT, and carries a max_fee ceiling for a network fee paid by the holder at settlement. The MINT and SEND kinds join SWAP, PULL, and BURN.
  3. The API surface. The separate SwapMsg and SwapQuery gRPC services are gone. Every transaction RPC, including the new claim messages, is on the module's Msg service, and every query RPC is on its Query service.

1. Single-transaction claim permits​

A claim permit is a signature over an action. The signing authority signs the message, and an executor submits it. The executor pays the standard transaction fee; the signer needs no balance and no transaction.

MessageSigning authorityEffect
MsgClaimMintThe denom adminMints amount of denom to mint_to.
MsgClaimSendThe balance holder (from)Sends amount of denom from from to to.
MsgClaimMintNFTThe class issuerMints a certificate into to and returns the new token_id.
MsgClaimSendNFTThe current certificate holder (from)Transfers the certificate to to.
MsgRevokeClaimThe signerPermanently invalidates an unused nonce.

The permit is not a Cosmos transaction signature. It is a separate secp256k1 signature over a domain-tagged, length-prefixed payload:

ElementValue
Curve and hashsecp256k1 over SHA-256 of the payload
Signature64-byte compact R‖S, lower-S; a high-S signature is rejected
Public key33-byte compressed secp256k1
Address bindingRIPEMD160(SHA256(pubkey)) must resolve to the claimed signer
Signed fieldsEvery field except executor and signature
Domain tagscenterchain/tokenfactory/claim-mint, claim-send, claim-mint-nft, claim-send-nft

Each field is prefixed with its length as a big-endian uint32, so two different field lists cannot collide by concatenation. The domain tag makes a mint signature unusable as a send signature.

Replay and freshness​

A claim permit is single-use and chain-bound:

ProtectionHow
Single useA 16-byte nonce chosen by the signer. The chain stores a per-signer used/{signer}/{nonce} marker in the same transaction as the action.
Cross-chain replaychain_id must equal the executing chain.
Expiryexpiry_height is an absolute block height. An expired permit is rejected.
Far-future expiryexpiry_height may be at most max_claim_expiry_blocks ahead of the executing height. The default is 14,400 blocks, about one day at the assumed six-second block time.
Cross-type replayThe domain tag binds the signature to one message type.
AttributionThe public key must hash to the claimed signer address.
CancellationMsgRevokeClaim writes a permanent revoked/{signer}/{nonce} tombstone. A revoked nonce can never be spent.

Used-nonce markers are pruned after the permit's expiry height has passed. Revocation tombstones are permanent. Both are carried in the module's genesis state, so an export/import does not reopen a replay window. A new chain still needs a new chain ID: a reset that reuses the old ID would let a permit signed for the old chain execute on the new one until its expiry.

Guards​

A claim is not a way around the chain's normal rules:

  • MsgClaimMint requires the signer to be the denom admin, and the recipient cannot be a module account.
  • MsgClaimSend is a normal bank send after the signature check. The blacklist and restricted-denom rules still apply, and the recipient cannot be a module account.
  • MsgClaimMintNFT requires the signer to be the class issuer and enforces the class, URI, and URI-hash limits.
  • MsgClaimSendNFT requires the signer to be the current owner and re-runs the NFT send checks: lock, blocklist, and restricted-class policy.
  • MsgRevokeClaim cannot revoke a nonce that was already spent, and a permit cannot be spent after its nonce is revoked.

2. The authorization rework​

MsgCreateAuthorization still opens a permission that a named executor settles later, but the terms and the settlement rules changed.

Kinds​

KindSettlementMeaning
SWAPMsgExecuteSwapTwo legs: consume the input and deliver the output to the holder.
PULLMsgExecutePullOne leg: move the input to the executor. Supply is unchanged.
BURNMsgExecuteBurnOne leg: burn the input. Supply shrinks.
MINTMsgExecuteMintGas authorization only. The mint action is a separate, executor-signed permit.
SENDMsgExecuteSendGas authorization only. The send action is a separate, executor-signed permit.

UNSPECIFIED is still rejected. A MINT or SEND authorization must not declare asset terms: it names only the holder who pays the settlement fee, the executor, the sequence, the chain, the fee ceiling, and the lifetime.

New and removed terms​

Fieldv1.2
max_feeNew. A positive canonical amount in ucen. The holder signs this ceiling for the network fee of TX2. It is not the asset budget.
valid_blocksNew. A relative lifetime in blocks. Zero resolves to default_authorization_blocks at creation time.
expire_heightNew in state. The absolute height computed at creation. An authorization is rejected after that height and pruned by BeginBlock.
attemptedNew in state. Set when a settlement transaction pays the holder's fee. A second settlement with the same authorization is rejected.
gas_fee_maxRemoved (reserved field 9).
gas_fee on MsgExecuteSwapRemoved (reserved field 7).
gas_fee and gas_mode on Authorization / queriesRemoved.

Defaults: default_authorization_blocks is 300, max_authorization_blocks is 10,000, and auth_free_blocks is 100. Creating an authorization consumes auth_base_gas (50,000) plus auth_gas_per_block (100) for every block beyond the free tier, so a longer-lived permission costs more at TX1.

No reservation, no lock​

Creating an authorization reserves nothing. It does not hold max_from against the holder's balance and does not lock an NFT input. The holder can spend the same funds or transfer the same token while the authorization is open; settlement then fails closed if the balance or ownership is no longer there.

The only read-only check at TX1 is the secondary-market lock: an NFT already locked by nftmarket cannot be promised to an executor. The tokenfactory no longer creates NFT locks of its own.

Settlement fee​

The holder named in the authorization pays the TX2 network fee, not the executor. The fee is charged in ucen by a tokenfactory ante decorator:

  • The transaction must contain exactly one MsgExecute* message, and an authz.MsgExec may not wrap one.
  • The fee must be at most the signed max_fee, and only ucen is accepted.
  • If the holder cannot cover the fee, the transaction is rejected. There is no fallback to the executor's balance.
  • Feegrant is ignored: the holder is the payer, and a fee granter is cleared.
  • The ante marks the authorization attempted when it charges the holder. A failed settlement still consumes the attempt; retrying requires a new authorization.
  • If no usable authorization is found, the executor pays the standard fee and the handler rejects the execution.

Settling​

MessageChecksEffect
MsgExecuteSwapused_from <= max_from, exact to_amount, kind SWAP, executor match, chain, not expired, not attempted.Consumes the input and delivers the output to the holder.
MsgExecutePullamount <= max_from, kind PULL, executor match, chain, not expired, not attempted.Moves the input to the executor. Nothing is burned.
MsgExecuteBurnamount <= max_from, kind BURN, executor is the denom admin or, for an NFT, the class issuer.Moves the input to the module and burns it.
MsgExecuteMintThe TX1 authorization is MINT and gas-only. A separate action permit is signed by the executor.Mints on behalf of the denom admin.
MsgExecuteSendThe TX1 authorization is SEND and gas-only. A separate action permit is signed by the executor.Sends from the executor, who must be the balance holder.

The MINT and SEND action permit uses the claim-permit signature contract: a nonce, chain_id, expiry_height, compressed public key, and signature over the domain-tagged payload. It is checked independently of the TX1 authorization, so the gas authorization cannot alter the action's amount, denom, or recipient.

Expiry and pruning​

BeginBlock prunes authorizations whose expire_height is below the current height and emits authorization_expired. The per-holder high-water mark is preserved, so a pruned sequence number is never reused. Queries also filter an expired authorization defensively, even if a prune has not run yet.

3. API surface changes​

The service split was removed. This is a breaking change for generated clients:

Beforev1.2
centerchain.tokenfactory.v1.SwapMsgThe RPCs are on centerchain.tokenfactory.v1.Msg.
centerchain.tokenfactory.v1.SwapQueryThe RPCs are on centerchain.tokenfactory.v1.Query.

Message type URLs are unchanged, so stored messages and existing type registrations still resolve. The gRPC service names changed, so a client must regenerate its stubs or point at the new service.

The REST gateway follows the same move:

  • Authorizations: /centerchain/tokenfactory/v1beta1/swaps/{holder}/authorizations, /authorizations/{order_seq}, and /high_water_mark.
  • NFT queries: /centerchain/nft/v1/..., with identifiers bound as query parameters because a class id can contain /.
  • Liquidity simulation: the two simulate queries are now POST endpoints.

Autocli exposes the new commands at the top level. The old tokenfactory swap subcommand is gone:

centerchaind tx tokenfactory create-authorization ...
centerchaind tx tokenfactory execute-swap ...
centerchaind tx tokenfactory execute-pull ...
centerchaind tx tokenfactory execute-burn ...
centerchaind tx tokenfactory execute-mint ...
centerchaind tx tokenfactory execute-send ...
centerchaind tx tokenfactory revoke-authorization ...

centerchaind tx tokenfactory claim-mint ...
centerchaind tx tokenfactory claim-send ...
centerchaind tx tokenfactory claim-mint-nft ...
centerchaind tx tokenfactory claim-send-nft ...
centerchaind tx tokenfactory revoke-claim ...

centerchaind query tokenfactory authorization ...
centerchaind query tokenfactory authorizations ...
centerchaind query tokenfactory high-water-mark ...

MsgSetSwapConfig and the SwapConfig query remain for compatibility, but the config type has no fields: technical_issuer and executors are reserved. The executor is chosen by the holder in each authorization; an allow-list, if one is needed, is an off-chain policy.

4. Cross-module effects​

AreaEffect
NFTA tokenfactory authorization no longer locks an NFT. Only nftmarket locks tokens. TX1 rejects an NFT that is already market-locked, and settlement fails closed if ownership or the lock changed.
NFT marketUnchanged listing behaviour. A listed token cannot be promised to a tokenfactory executor while the listing lock is present.
IncomeUnchanged. Claim sends and execution sends still run through the bank path and its send restrictions.
LiquidityUnchanged. The liquidity module's internal LP-share burn is not a holder balance.

5. Technical scope​

New messages and state

  • proto/centerchain/tokenfactory/v1/tx.proto — the Msg service and every request/response message, including the five claim messages and the authorization execution messages. AuthorizationKind, NFTScope, and SwapConfig live here too.
  • proto/centerchain/tokenfactory/v1/swap.proto — the stored Authorization, SwapGenesis, ClaimMarker, and query messages. The Msg request/response messages moved to tx.proto, so this file is no longer a second message surface.
  • x/tokenfactory/keeper/claim.go — claim permits, action permits, replay markers, revocation, and pruning.
  • x/tokenfactory/keeper/fee_ante.go — the holder-paid settlement fee.
  • x/tokenfactory/keeper/swap.go — expiry, no-reservation settlement, and prune support.
  • x/tokenfactory/types/params.go — authorization and claim parameters.

Surface

  • x/tokenfactory/module/autocli.go — the flattened command tree.
  • proto/centerchain/liquidity/v1/query.proto, proto/centerchain/nft/v1/nft.proto, and proto/centerchain/tokenfactory/v1/query.proto — REST annotations.
  • internal/openapi/static/openapi.json — regenerated gateway document.
  • ts-client — regenerated protobuf clients.

Consensus version

tokenfactory stays at consensus version 3. v1.2 changes the state schema but ships no migration, because every v1.2 network starts from a new genesis. A network that wanted to upgrade a live v1.1 chain in place would need a migration and a consensus-version bump; that path is not part of this release.

6. Applying v1.2​

v1.2 is not applied through an upgrade plan. The state and protobuf changes are incompatible with a v1.1 chain, so the testnet was reset:

ItemValue
Chain IDcenterchain-testnet-2
Deployed app versionv1.2-testnet2-fdb815e (a build label, not a signed release)
Genesis SHA-2563e79772a3e05c281d56e7f65c5d92ca5d175215821f74a6198b0e95070224fa7
Validators4, all on a single host
Initial ucen supply1,000,000 CEN: owner 590,000; faucet 10,000; validators 0–3 × 100,000 self-delegated

The previous centerchain-testnet-1 was stopped, and its data was retained. Its chain ID, genesis, and state are not compatible with v1.2.

Production does not exist. If it is launched, it must start from a v1.2 genesis with its own chain ID; it cannot be created by upgrading the testnet. The new chain ID is also what invalidates every claim permit and authorization signed for the old chain.

The registered software-upgrade plans remain v0.55 and v1.1. There is no v1.2 handler to stage, and the release workflow has not published a signed v1.2 artifact. Do not advertise a download URL until it does.

7. Verifying the result​

After a v1.2 genesis:

  • Block production continues, and /status reports centerchain-testnet-2 (or the chain ID of the network under test).
  • A MsgCreateAuthorization with valid_blocks = 0 stores the default 300-block lifetime, and one above max_authorization_blocks is rejected.
  • An authorization past its expire_height is rejected and eventually pruned; its order_seq is not reused.
  • Creating an authorization does not reduce the holder's spendable balance and does not lock an NFT.
  • A TX2 charges the holder in ucen, respects max_fee, and rejects a second attempt with ErrAuthorizationAttempted (1126).
  • A TX2 with more than one MsgExecute*, or with one nested in authz.MsgExec, is rejected with ErrExecuteTxShape (1127).
  • MsgExecutePull moves the input to the executor without changing supply; MsgExecuteBurn requires the issuer role and shrinks supply.
  • MsgExecuteMint and MsgExecuteSend require a matching MINT or SEND gas authorization and a valid action-permit signature.
  • MsgClaimMint and MsgClaimSend execute with the signer's signature and the executor's fee; a reused nonce fails with ErrClaimNonceUsed (1121), and a revoked nonce fails with ErrClaimRevoked (1128).
  • MsgClaimSendNFT still enforces the lock, blocklist, and restricted-class rules.
  • A claim permit with an expiry more than max_claim_expiry_blocks ahead fails with ErrClaimExpiryTooFar (1129).

8. Client migration checklist​

  1. Regenerate gRPC clients from the v1.2 protos. Move calls from SwapMsg and SwapQuery to Msg and Query; the claim RPCs are on Msg.
  2. Replace gas_fee_max with max_fee, and add valid_blocks (or send zero to accept the default).
  3. Remove gas_fee from MsgExecuteSwap; the fee is the transaction fee, paid by the authorization holder in ucen.
  4. Fund the authorization holder with ucen before settlement. The executor cannot pay on the holder's behalf, and feegrant is ignored.
  5. Send exactly one MsgExecute* per transaction and never wrap one in authz.MsgExec.
  6. Treat a failed settlement as terminal for that authorization: create a new one instead of retrying.
  7. For a signer who should not submit a transaction or hold ucen, use a MsgClaim* permit and let an executor submit it.
  8. Re-query authorizations after the upgrade: expire_height and attempted replace the removed gas fields.