v1.2 — Claim permits and the authorization rework
| Field | Value |
|---|---|
| Upgrade plan name | None. v1.2 is applied by a genesis reset with a new chain ID. No v1.2 software-upgrade plan is registered. |
| Upstream commit | 6a8a6e0 — fix(tokenfactory): use executor as action permit signer in execute mint and send |
| Consensus effect | Yes. The authorization state schema, the message surface, the ante handler, and the gRPC service layout change. |
| Store upgrade | None. A network starts from a v1.2 genesis; there is no migration from v1.1 state. |
| Applies to | centerchain-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.
- 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. - 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_feeceiling for a network fee paid by the holder at settlement. TheMINTandSENDkinds joinSWAP,PULL, andBURN. - The API surface. The separate
SwapMsgandSwapQuerygRPC services are gone. Every transaction RPC, including the new claim messages, is on the module'sMsgservice, and every query RPC is on itsQueryservice.
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.
| Message | Signing authority | Effect |
|---|---|---|
MsgClaimMint | The denom admin | Mints amount of denom to mint_to. |
MsgClaimSend | The balance holder (from) | Sends amount of denom from from to to. |
MsgClaimMintNFT | The class issuer | Mints a certificate into to and returns the new token_id. |
MsgClaimSendNFT | The current certificate holder (from) | Transfers the certificate to to. |
MsgRevokeClaim | The signer | Permanently 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:
| Element | Value |
|---|---|
| Curve and hash | secp256k1 over SHA-256 of the payload |
| Signature | 64-byte compact R‖S, lower-S; a high-S signature is rejected |
| Public key | 33-byte compressed secp256k1 |
| Address binding | RIPEMD160(SHA256(pubkey)) must resolve to the claimed signer |
| Signed fields | Every field except executor and signature |
| Domain tags | centerchain/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:
| Protection | How |
|---|---|
| Single use | A 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 replay | chain_id must equal the executing chain. |
| Expiry | expiry_height is an absolute block height. An expired permit is rejected. |
| Far-future expiry | expiry_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 replay | The domain tag binds the signature to one message type. |
| Attribution | The public key must hash to the claimed signer address. |
| Cancellation | MsgRevokeClaim 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:
MsgClaimMintrequires the signer to be the denom admin, and the recipient cannot be a module account.MsgClaimSendis a normal bank send after the signature check. The blacklist and restricted-denom rules still apply, and the recipient cannot be a module account.MsgClaimMintNFTrequires the signer to be the class issuer and enforces the class, URI, and URI-hash limits.MsgClaimSendNFTrequires the signer to be the current owner and re-runs the NFT send checks: lock, blocklist, and restricted-class policy.MsgRevokeClaimcannot 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
| Kind | Settlement | Meaning |
|---|---|---|
SWAP | MsgExecuteSwap | Two legs: consume the input and deliver the output to the holder. |
PULL | MsgExecutePull | One leg: move the input to the executor. Supply is unchanged. |
BURN | MsgExecuteBurn | One leg: burn the input. Supply shrinks. |
MINT | MsgExecuteMint | Gas authorization only. The mint action is a separate, executor-signed permit. |
SEND | MsgExecuteSend | Gas 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
| Field | v1.2 |
|---|---|
max_fee | New. A positive canonical amount in ucen. The holder signs this ceiling for the network fee of TX2. It is not the asset budget. |
valid_blocks | New. A relative lifetime in blocks. Zero resolves to default_authorization_blocks at creation time. |
expire_height | New in state. The absolute height computed at creation. An authorization is rejected after that height and pruned by BeginBlock. |
attempted | New in state. Set when a settlement transaction pays the holder's fee. A second settlement with the same authorization is rejected. |
gas_fee_max | Removed (reserved field 9). |
gas_fee on MsgExecuteSwap | Removed (reserved field 7). |
gas_fee and gas_mode on Authorization / queries | Removed. |
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 anauthz.MsgExecmay not wrap one. - The fee must be at most the signed
max_fee, and onlyucenis 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
attemptedwhen 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
| Message | Checks | Effect |
|---|---|---|
MsgExecuteSwap | used_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. |
MsgExecutePull | amount <= max_from, kind PULL, executor match, chain, not expired, not attempted. | Moves the input to the executor. Nothing is burned. |
MsgExecuteBurn | amount <= 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. |
MsgExecuteMint | The TX1 authorization is MINT and gas-only. A separate action permit is signed by the executor. | Mints on behalf of the denom admin. |
MsgExecuteSend | The 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:
| Before | v1.2 |
|---|---|
centerchain.tokenfactory.v1.SwapMsg | The RPCs are on centerchain.tokenfactory.v1.Msg. |
centerchain.tokenfactory.v1.SwapQuery | The 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
POSTendpoints.
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
| Area | Effect |
|---|---|
| NFT | A 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 market | Unchanged listing behaviour. A listed token cannot be promised to a tokenfactory executor while the listing lock is present. |
| Income | Unchanged. Claim sends and execution sends still run through the bank path and its send restrictions. |
| Liquidity | Unchanged. 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— theMsgservice and every request/response message, including the five claim messages and the authorization execution messages.AuthorizationKind,NFTScope, andSwapConfiglive here too.proto/centerchain/tokenfactory/v1/swap.proto— the storedAuthorization,SwapGenesis,ClaimMarker, and query messages. The Msg request/response messages moved totx.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, andproto/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:
| Item | Value |
|---|---|
| Chain ID | centerchain-testnet-2 |
| Deployed app version | v1.2-testnet2-fdb815e (a build label, not a signed release) |
| Genesis SHA-256 | 3e79772a3e05c281d56e7f65c5d92ca5d175215821f74a6198b0e95070224fa7 |
| Validators | 4, all on a single host |
Initial ucen supply | 1,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
/statusreportscenterchain-testnet-2(or the chain ID of the network under test). - A
MsgCreateAuthorizationwithvalid_blocks = 0stores the default 300-block lifetime, and one abovemax_authorization_blocksis rejected. - An authorization past its
expire_heightis rejected and eventually pruned; itsorder_seqis 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, respectsmax_fee, and rejects a second attempt withErrAuthorizationAttempted(1126). - A TX2 with more than one
MsgExecute*, or with one nested inauthz.MsgExec, is rejected withErrExecuteTxShape(1127). MsgExecutePullmoves the input to the executor without changing supply;MsgExecuteBurnrequires the issuer role and shrinks supply.MsgExecuteMintandMsgExecuteSendrequire a matchingMINTorSENDgas authorization and a valid action-permit signature.MsgClaimMintandMsgClaimSendexecute with the signer's signature and the executor's fee; a reused nonce fails withErrClaimNonceUsed(1121), and a revoked nonce fails withErrClaimRevoked(1128).MsgClaimSendNFTstill enforces the lock, blocklist, and restricted-class rules.- A claim permit with an expiry more than
max_claim_expiry_blocksahead fails withErrClaimExpiryTooFar(1129).
8. Client migration checklist
- Regenerate gRPC clients from the v1.2 protos. Move calls from
SwapMsgandSwapQuerytoMsgandQuery; the claim RPCs are onMsg. - Replace
gas_fee_maxwithmax_fee, and addvalid_blocks(or send zero to accept the default). - Remove
gas_feefromMsgExecuteSwap; the fee is the transaction fee, paid by the authorization holder inucen. - Fund the authorization holder with
ucenbefore settlement. The executor cannot pay on the holder's behalf, and feegrant is ignored. - Send exactly one
MsgExecute*per transaction and never wrap one inauthz.MsgExec. - Treat a failed settlement as terminal for that authorization: create a new one instead of retrying.
- For a signer who should not submit a transaction or hold
ucen, use aMsgClaim*permit and let an executor submit it. - Re-query authorizations after the upgrade:
expire_heightandattemptedreplace the removed gas fields.