Two-phase execution
A move of a CenterChain asset outside a liquidity pool happens in two transactions. The holder signs the first and grants a limited permission; a named executor signs the second and settles against it. The chain never computes a price — it only enforces the bounds the holder set.
This is implemented in tokenfactory; this page explains the shape and the reasoning.
| Phase | Message | Signer | Effect |
|---|---|---|---|
| TX1 | MsgCreateAuthorization | holder | Reserves funds or locks an NFT, and fixes one purpose. Moves nothing out of the holder's control. |
| TX2 | MsgExecuteSwap | executor | Burns or transfers the input and mints the output, inside the authorization's bounds. |
| TX2 | MsgExecutePull | executor | Moves the input to the executor and burns nothing. |
| TX2 | MsgExecuteBurn | executor | Burns the input. |
The executor list is set by governance through MsgSetSwapConfig. An
authorization names its executor explicitly, and the settlement re-checks that
name.
Three kinds
An authorization carries a kind that fixes which settlement message may spend
it. MsgCreateAuthorization requires the kind explicitly; UNSPECIFIED is not
accepted, so the purpose is never inferred.
| Kind | Settlement | What it does |
|---|---|---|
SWAP | MsgExecuteSwap | Two legs: the input is consumed and the output is minted to the holder. |
PULL | MsgExecutePull | One leg: the input is transferred to the executor. Supply is unchanged. |
BURN | MsgExecuteBurn | One leg: the input is burned. Supply shrinks. The executor must be the input's denom admin, or the class issuer for an NFT. |
Because the kind is part of what the holder signs, a SWAP authorization can
never be spent by MsgExecutePull or MsgExecuteBurn, and the reverse holds
too. The settlement re-checks the kind and rejects a mismatch with
ErrUnauthorized (1103).
A PULL or BURN has no output leg and a zero gas fee: the holder names an
input, a ceiling, and an executor, and nothing is minted back. Both release the
full reservation, so unspent budget returns to the holder.
PULL and BURN exist because of the holder-protection rule: a holder may grant
a counterparty a limited right to move or burn a specific amount of their asset,
but only by signing for it. See
Holder asset protection.
The terms
kind // SWAP, PULL, or BURN: fixes which settlement may spend it
holder // the granter, and the only address a SWAP output may go to
order_seq // per-holder monotonic sequence, never reused
chain_id // domain separation against cross-chain replay
executor // the account allowed to settle
from_denom // the input, empty for an NFT input
to_denom // the output, empty for an NFT output
to_amount // the exact amount the holder receives
max_from // the ceiling on the total input
gas_fee_max // the ceiling on the gas reimbursement
from_nft // NFT input: class id and token id
to_nft // NFT output: class id, token id assigned at settlement
The shape is one-sided by construction. The holder fixes what they receive and
caps what they pay. There is no min_to, and the price is what moves between
them.
The gas fee is paid in the input denom, not in ucen, and the executor
charges an amount no greater than the holder's ceiling. gas_fee_max must be
strictly below max_from, so the gas ceiling can never consume the swap budget
itself.
Five properties
Single use
order_seq must be greater than the holder's high-water mark, and the high-water
mark only moves forward. Sequence numbers are never reused, even after pruning,
so a delayed TX2 cannot arrive with an old id and consume a fresh authorization.
consumed is set in the same transaction as the settlement. A retried TX2 sees
it and fails.
Bounded
The holder's spendable balance must cover the max_from reservation, and the
reservation is enforced at the store layer. Execution additionally checks
used_from <= max_from and gas_fee <= gas_fee_max, both in the same handler
that performs the transfer.
A better price is never rejected. If the executor settles at a price inside the limit, the trade happens; the limit is a ceiling on cost, not a requirement to pay exactly it.
Scoped to the destination
The output always goes to the holder. The executor cannot redirect it, and the
denoms cannot be changed — both are inside the authorization that the holder
signed. A PULL or BURN has no output, and the same holds for its single leg:
the executor receives the pulled amount or burns it, and it cannot redirect
either, because the input and the amount are what the holder signed.
Re-checked at settlement
TX2 does not trust the state that existed at TX1. It re-runs the blacklist and restriction checks, re-checks that the executor matches, and re-checks the budget. A holder who was blacklisted between the two phases cannot settle.
Domain separated
chain_id is mandatory and must equal the current chain, so an authorization
cannot be replayed on another chain.
Revocation
The holder can cancel at any time with MsgRevokeAuthorization. It releases the
reservation or unlocks the NFT and marks the authorization revoked.
Consumption and revocation are mutually exclusive: whichever transaction lands first wins, and both are evented. There is no third outcome where an authorization ends up both spent and cancelled.
The flow
A PULL or BURN follows the same two transactions with a single leg: the
authorization reserves or locks the input, and the settlement releases the
reservation, moves or burns the input, and marks the authorization consumed.
What it does not protect against
- Price movement. The chain does not know a price. An authorization with a
tight
max_fromsimply fails to settle, and the holder re-authorizes at a new price. A stale authorization does not execute at a bad price; it fails. - A compromised executor key. The executor can settle inside the authorization, but it cannot exceed it, and it never sees the holder's key. The worst case is a trade the holder already approved.
- A holder who authorizes and then spends the balance. The reservation makes that spend fail rather than stranding the executor.