Two-phase execution
A move of a CenterChain asset outside a liquidity pool can happen in two transactions. The holder signs the first and grants a limited, expiring 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. The one-transaction alternative is the claim permit.
| Phase | Message | Signer | Effect |
|---|---|---|---|
| TX1 | MsgCreateAuthorization | holder | Stores a permission with a purpose, a fee ceiling, and an expiry. Moves nothing and reserves nothing. |
| TX2 | MsgExecuteSwap | executor | Consumes the input and delivers 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. |
| TX2 | MsgExecuteMint | executor | Mints under a MINT gas authorization and a separate action permit. |
| TX2 | MsgExecuteSend | executor | Sends under a SEND gas authorization and a separate action permit. |
The holder names the executor in the authorization, and the settlement re-checks
that name. There is no governance executor list in v1.2: SwapConfig has no
fields, and an allow-list, if one is needed, is enforced off-chain.
Five 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 delivered 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. |
MINT | MsgExecuteMint | Gas only. The mint action is a separate permit signed by the denom admin. |
SEND | MsgExecuteSend | Gas only. The send action is a separate permit signed by the balance holder. |
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).
MINT and SEND are different from the other three. They carry no asset terms:
they exist so the holder can pay the TX2 network fee for an action that is
authorized by a separate, executor-signed permit. See
Claim permits.
The terms
kind // SWAP, PULL, BURN, MINT, or SEND
holder // the granter, and for SWAP the only address the 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
max_fee // the ceiling on the TX2 network fee, in ucen
valid_blocks // relative lifetime; zero means the default
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.
max_fee is the holder's ceiling for the network fee, paid in ucen at
TX2. It is not an asset budget and it is not paid in the input denom. The asset
budget is max_from, and used_from is checked against it at settlement.
valid_blocks is relative: zero resolves to default_authorization_blocks
(300), and a value above max_authorization_blocks (10,000) is rejected. The
stored expire_height is absolute.
Six 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.
One attempt
TX2 is charged to the holder's max_fee budget, and the ante marks the
authorization attempted when it charges. A settlement that fails after that
point cannot be retried with the same authorization; the holder must create a new
one. The attempt marker is stored with the authorization and survives genesis
export/import.
A TX2 that cannot charge the holder never reaches the handler, so it does not consume an attempt. The executor cannot pay in the holder's place.
Bounded
TX1 does not reserve the holder's balance and does not lock an NFT. The holder can spend the balance or transfer the token while the authorization is open. Settlement fails closed if the funds or ownership are no longer there.
At settlement the handler checks used_from <= max_from and, for a swap, that
to_amount is exactly the authorized amount. A better price is never rejected:
if the executor settles 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 of a swap 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, re-checks the budget, and checks the expiry. A holder who was blacklisted between the two phases cannot settle. An NFT that was transferred, burned, or market-locked cannot be settled.
Expiring and domain separated
Every authorization has an absolute expire_height. After that height the
settlement is rejected with ErrAuthorizationExpired (1123), and BeginBlock
prunes the record and emits authorization_expired. The high-water mark is
preserved, so the sequence number is not reused.
chain_id is mandatory and must equal the current chain, so an authorization
cannot be replayed on another chain.
Fees
The two transactions have different fee models:
- TX1 pays duration-based gas. Creating a longer-lived authorization consumes
more gas:
auth_base_gasplusauth_gas_per_blockfor every block beyondauth_free_blocks. - TX2 is submitted and signed by the executor, but the holder pays the
network fee. The fee is charged in
ucen, must not exceedmax_fee, and is deducted by a tokenfactory ante decorator. Feegrant is ignored, and an unfunded holder causes the transaction to fail.
A settlement transaction must contain exactly one MsgExecute* message. An
authz.MsgExec may not wrap one, so a nested execution cannot hide the executor
from the fee and attempt rules.
Revocation
The holder can cancel at any time with MsgRevokeAuthorization. It marks the
authorization revoked. There is no reservation to release and no tokenfactory NFT
lock to clear; a legacy lock created by a pre-v1.2 authorization is released if
it matches the order id.
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 records the input and budget, and the settlement moves or burns the
input and marks the authorization consumed. A MINT or SEND settlement carries
its own action permit and consumes the gas authorization.
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. Because TX1 reserves nothing, the holder can spend the balance. The settlement then fails closed rather than stranding the executor. A counterparty that needs a guarantee must obtain it off-chain.
- An unfunded holder at settlement. The holder pays the TX2 fee, so the
holder must hold enough
ucenwhen the executor settles. The executor cannot substitute its own balance.