Skip to main content
Version: 0.1.0

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.

PhaseMessageSignerEffect
TX1MsgCreateAuthorizationholderReserves funds or locks an NFT. Moves no funds out of the holder's control.
TX2MsgExecuteSwapexecutorBurns 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_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.