Insight
A swap outside a pool takes two transactions
Moving one CenterChain denom into another outside a liquidity pool is an authorization plus a settlement. Here are the two messages, the five properties that make them hold, and the three attacks they do not stop.
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 does not compute a price. It only enforces the bounds the holder set.
This post walks both messages, the five properties that make the pair hold, and the three things the pair does not protect against.
Source: docs/concepts/two-phase-execution.md:10-13, docs/modules/tokenfactory.md:142-145
The two messages
| 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. |
| Cancel | MsgRevokeAuthorization |
holder |
Releases the reservation or unlocks the NFT, and marks the authorization revoked. |
The executor list is set by governance through MsgSetSwapConfig. An authorization names its executor
explicitly, and the settlement re-checks that name. The chain is not choosing a counterparty for the
holder.
Source: docs/concepts/two-phase-execution.md:18-25, docs/modules/tokenfactory.md:142-155
The shape is one-sided on purpose
The terms of an authorization are: the holder, a per-holder sequence, a chain id, the executor, the input and output denoms, the exact output amount, a ceiling on the total input, and a ceiling on the gas reimbursement.
The shape is one-sided by construction. The holder fixes what they receive and caps what they pay. There
is no min_to field, so the price is whatever moves between the two of them.
The gas fee is paid in the input denom, not in ucen, and gas_fee_max must be strictly below max_from.
That is why the gas ceiling can never consume the swap budget itself.
Source: docs/concepts/two-phase-execution.md:27-50, docs/modules/tokenfactory.md:157-184
Five properties
Single use. order_seq must be greater than the holder’s high-water mark, and the 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 written in the same transaction as the burn and the
mint, so 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 also 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. 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 the holder signed.
Re-checked at settlement. TX2 does not trust the state that existed at TX1. It re-runs the blacklist and the 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.
Source: docs/concepts/two-phase-execution.md:52-89, docs/modules/tokenfactory.md:186-235
Revocation has no third outcome
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 state in which an authorization ends up both spent and cancelled.
Source: docs/concepts/two-phase-execution.md:91-98, docs/modules/tokenfactory.md:229-235
What this does not protect against
Price movement. The chain does not know a price. An authorization with a tight max_from simply 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 with a promise the holder no longer has the funds for.
Source: docs/concepts/two-phase-execution.md:118-127
What you can and cannot reach today
Two honest notes about reaching this subsystem, both taken from the module documentation rather than from a plan:
- The swap query service has no HTTP annotation. The swap state is reachable over gRPC only.
- The swap messages have no autocli entry. They are built as JSON and submitted with
centerchaind tx broadcastor a gRPC client.
Source: docs/modules/tokenfactory.md:244-255, 330-331
The one public network is centerchain-testnet-1, and its tokens carry no value. Nothing here is a claim
about a network that carries real value, because no such network is published.
Source: docs/start-here/network-status.md:18-19, 90-95
Where to check this yourself
- The model, in the concepts section:
docs/concepts/two-phase-execution.md - The module implementation:
docs/modules/tokenfactory.md:142-242 - The send rules both phases re-run: one function decides every send