Skip to main content
Version: 1.2.0

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.

PhaseMessageSignerEffect
TX1MsgCreateAuthorizationholderStores a permission with a purpose, a fee ceiling, and an expiry. Moves nothing and reserves nothing.
TX2MsgExecuteSwapexecutorConsumes the input and delivers the output, inside the authorization's bounds.
TX2MsgExecutePullexecutorMoves the input to the executor and burns nothing.
TX2MsgExecuteBurnexecutorBurns the input.
TX2MsgExecuteMintexecutorMints under a MINT gas authorization and a separate action permit.
TX2MsgExecuteSendexecutorSends 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.

KindSettlementWhat it does
SWAPMsgExecuteSwapTwo legs: the input is consumed and the output is delivered to the holder.
PULLMsgExecutePullOne leg: the input is transferred to the executor. Supply is unchanged.
BURNMsgExecuteBurnOne leg: the input is burned. Supply shrinks. The executor must be the input's denom admin, or the class issuer for an NFT.
MINTMsgExecuteMintGas only. The mint action is a separate permit signed by the denom admin.
SENDMsgExecuteSendGas 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_gas plus auth_gas_per_block for every block beyond auth_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 exceed max_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_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. 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 ucen when the executor settles. The executor cannot substitute its own balance.