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 broadcast or 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