Two-phase execution
A swap between two CenterChain denoms 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. Moves no funds out of the holder's control. |
| TX2 | MsgExecuteSwap | executor | Burns the input and mints the output, atomically, inside the authorization's bounds. |
The executor list is set by governance through MsgSetSwapConfig. An
authorization names its executor explicitly, and the settlement re-checks that
name.
The terms
holder // the granter, and 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
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 burn and the mint. 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.
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
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.