Claim permits
A claim permit is a signature that authorizes one action without the signer
submitting a transaction. The signing authority signs the action, an executor
submits it in one transaction, and the executor pays the standard network fee.
The signer needs no ucen and no transaction of its own.
This is the single-transaction path. The two-transaction path is the authorization, where the holder signs TX1 and the executor settles TX2 later. The two are separate: a claim permit is not a compressed authorization, and it does not create authorization state.
The implementation is in tokenfactory.
The messages
| Message | Signing authority | Effect |
|---|---|---|
MsgClaimMint | The denom admin (owner) | Mints amount of denom to mint_to. |
MsgClaimSend | The balance holder (from) | Sends amount of denom 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. |
MsgRevokeClaim is a normal transaction signed by the signer. It carries no
permit signature: the transaction signature is the authorization.
The flow
The signature contract
The permit is not a Cosmos SignDoc signature. It is a separate signature over
a deterministic payload:
| Element | Value |
|---|---|
| Curve | secp256k1 |
| Hash | SHA-256 over the payload |
| Signature | 64-byte compact R‖S, lower-S; a high-S signature is rejected |
| Public key | 33-byte compressed secp256k1 |
| Address | RIPEMD160(SHA256(pubkey)), bech32-encoded with the chain prefix |
| Signed fields | Every field except executor and signature |
| Encoding | Each field is uint32_be(length) ‖ bytes; fields are concatenated in a fixed order |
The payload starts with a domain tag, so a signature for one message type cannot be replayed as another:
| Message | Domain tag |
|---|---|
MsgClaimMint | centerchain/tokenfactory/claim-mint |
MsgClaimSend | centerchain/tokenfactory/claim-send |
MsgClaimMintNFT | centerchain/tokenfactory/claim-mint-nft |
MsgClaimSendNFT | centerchain/tokenfactory/claim-send-nft |
MsgExecuteMint action permit | centerchain/tokenfactory/execute-mint |
MsgExecuteSend action permit | centerchain/tokenfactory/execute-send |
Field order
| Message | Fields, in order |
|---|---|
MsgClaimMint | owner, denom, amount, mint_to, nonce, chain_id, expiry_height, pubkey |
MsgClaimSend | from, denom, amount, to, nonce, chain_id, expiry_height, pubkey |
MsgClaimMintNFT | issuer, class_id, to, uri, uri_hash, nonce, chain_id, expiry_height, pubkey |
MsgClaimSendNFT | from, class_id, token_id, to, nonce, chain_id, expiry_height, pubkey |
MsgExecuteMint | holder, denom, amount, mint_to, nonce, chain_id, expiry_height, pubkey |
MsgExecuteSend | holder, denom, amount, to, nonce, chain_id, expiry_height, pubkey |
nonce is 16 raw bytes. expiry_height is an 8-byte big-endian uint64.
pubkey is 33 raw bytes. Strings are UTF-8. Amounts are canonical decimal
strings, with no leading zeros.
field(x) = uint32_be(len(x)) || x
payload = field(domain) || field(field_1) || ... || field(field_n)
signature = secp256k1_sign(sha256(payload), private_key)
The public key must resolve to the address named as the signing authority. A signature cannot be attributed to an address that did not produce it.
Action permits for ExecuteMint and ExecuteSend
The MINT and SEND authorizations are gas-only. The action itself is
authorized by a separate permit carried inside MsgExecuteMint or
MsgExecuteSend. That permit follows the same signature contract, but its
holder field is the TX1 fee payer and the transaction signer is the executor
who performs the action.
| Message | Action signer | Guard |
|---|---|---|
MsgExecuteMint | The denom admin | The signer must be the denom admin. |
MsgExecuteSend | The balance holder | The signer is the from of the bank send. |
The action permit is checked independently of the TX1 gas authorization. TX1 cannot change the denom, amount, or recipient in the action permit.
Replay and freshness
| Axis | Mechanism |
|---|---|
| Single use | A 16-byte nonce per signer. The chain stores claim/used/{signer}/{nonce} in the same transaction as the action. |
| Cross-chain | chain_id must equal the executing chain. |
| Expiry | expiry_height is absolute. A permit is rejected once the current height is greater than it. |
| Far-future expiry | expiry_height may be at most max_claim_expiry_blocks ahead of the executing height. The default is 14,400 blocks. |
| Purpose | The domain tag separates mint, send, NFT mint, and NFT send. |
| Attribution | The public key must hash to the claimed signer address. |
| Revocation | MsgRevokeClaim writes a permanent tombstone for an unused nonce. |
| Atomicity | The nonce marker and the action commit in the same transaction. If the action fails, both roll back and the nonce remains unused. |
Used-nonce markers are pruned after their expiry height. Revocation tombstones
are permanent, because the chain cannot know how far in the future a permit for
that nonce may have been signed. Both markers are exported in genesis and
restored on import; a new chain must use a new chain_id so permits signed for
the old chain cannot execute.
Gas
The executor pays the standard transaction fee. The signer pays nothing and needs
no ucen balance. This is different from the two-phase path, where the holder
named in the authorization pays the TX2 fee.
Guards
A claim permit is not a bypass:
- A fungible mint requires the signer to be the denom admin.
- An NFT mint requires the signer to be the class issuer.
- A fungible send is a normal bank send: blacklist and restricted-denom rules still apply.
- An NFT send re-runs the lock, blocklist, and restricted-class checks.
- A recipient cannot be a module account.
- Amounts must be positive and canonical.
- An NFT mint enforces the class-id, URI, and URI-hash limits.
Limits
| Field | Limit |
|---|---|
| Nonce | 16 bytes |
| Public key | 33 bytes, compressed |
| Signature | 64 bytes, lower-S |
| Chain ID | 1 to 128 characters |
| Expiry | Non-zero, and at most max_claim_expiry_blocks ahead |
| Class ID | 1 to 256 bytes |
| Token ID | 1 to 256 bytes |
| URI | At most 512 bytes |
| URI hash | At most 128 bytes |
Client checklist
- Build the payload with the exact field order and the correct domain tag.
- Length-prefix every field with a big-endian
uint32. - Hash with SHA-256 and sign with secp256k1, producing a 64-byte
R‖Swith lower-S. - Send the 33-byte compressed public key, and confirm it hashes to the signer address.
- Choose a fresh 16-byte nonce per signer. Reusing one fails.
- Set
expiry_heightto an absolute block height, no more thanmax_claim_expiry_blocksahead. - Set
chain_idto the executing chain. A permit for another chain is rejected. - Let the executor submit the message and pay the fee.
- If a permit may have leaked, revoke its nonce before it is spent.