How it works

One function decides every send

Every send on CenterChain, bank, NFT, and swap settlement alike, passes through one function. The first rule that matches decides. There is no second, weaker path for a bank denom, and no identifier to bypass it with.

The decision path

  1. 01

    Send kind

    A bank send, an NFT send and a swap settlement all reach the same function.

  2. 02

    Recipient blocked

    A blacklist entry on that denom rejects the send for anyone.

  3. 03

    Sender blocked

    A blacklist entry rejects the send for anyone, including the denom admin.

  4. 04

    Denom restricted

    The admin, a delegate, or an internal swap leg passes this test.

  5. 05

    Recipient already holds it

    Existing holders keep trading. Every other address is rejected.

Every no branch ends in allow, every yes branch ends in reject. Nothing after the function can widen it.

The blacklist is a list of denoms

  • Being blocked on one token has no effect on any other. A denom with no authority metadata is never subject to the check.
  • Only the denom admin can add or remove an entry, and an account cannot blacklist itself.
  • Adding is idempotent and removing decrements a global counter. The cap is 100,000 blacklisted addresses across the whole chain.
  • The check is absolute. It applies under an open policy, to admins, to delegates, and to the module authority.

Closing circulation without freezing holders

is_restricted is set by the denom admin. When it is set, a denom can only be sent to an address that already holds a balance of it.

Three senders pass anyway: the admin, one of the delegates, or an internal leg of a swap. This is the on-chain circulation control: an issuer can close a token to new addresses while leaving existing holders able to trade among themselves.

This is not a KYC list. The rule tests a balance, not an identity. The chain can prove that a recipient already holds the token; it cannot prove who that recipient is. Treating it as an identity check would be wrong in both directions: it blocks nothing extra, and it claims an assurance the chain cannot make.

Two-phase execution

A swap between two CenterChain denoms outside a liquidity pool happens in two transactions.

TX1

The holder signs the authorization

MsgCreateAuthorization reserves the funds or locks the NFT.

TX2

The named executor settles

MsgExecuteSwap burns the input and mints the output.

MsgCreateAuthorization is signed by the holder, and it moves no funds out of the holder's control. MsgExecuteSwap is signed by the executor, inside the bounds the holder set. The executor list is set by governance, and the settlement re-checks the name.

  1. 01

    Single use

    order_seq must be greater than the holder high-water mark, and the mark only moves forward. Sequence numbers are never reused, even after pruning, so a delayed settlement 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 retry sees it and fails.

  2. 02

    Bounded

    The spendable balance must cover the max_from reservation, and execution checks used_from <= max_from and gas_fee <= gas_fee_max inside 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.

  3. 03

    Scoped to the destination

    The output always goes to the holder. The executor cannot redirect it, and the denoms cannot be changed, because both are inside the authorization the holder signed.

  4. 04

    Re-checked at settlement

    The second transaction does not trust the state that existed at the first. It re-runs the blacklist and the restriction checks, re-checks that the executor matches, and re-checks the budget. A holder blacklisted between the two phases cannot settle.

  5. 05

    Domain separated

    chain_id is mandatory and must equal the current chain, so an authorization cannot be replayed on another chain.

The chain never computes a price. It only enforces the bounds the holder set, and the holder can revoke an authorization at any time with MsgRevokeAuthorization, which releases the reservation or unlocks the NFT.

The reservation is enforced in the store

A fungible swap authorization reserves max_from against the holder balance. The reservation is enforced at the store layer: a wrapper around the bank key value store rejects any write that would drop a balance below the amount open authorizations have reserved.

This holds regardless of which code path moved the funds. A transfer through the automated market maker, a direct send, a staking unbonding: anything that decreases the balance below the reservation fails.

There is no account level freeze

Per-denom blacklist

One denom, one address. No send in that denom, by anyone.

is_restricted

The whole denom. No transfer to a new address, by anyone except the admin and the delegates.

Income blocklist check

The claimant. A claim is rejected outright for a blocklisted holder.

A transfer barrier is not immutability. An admin can still burn, force transfer, or change the admin of a restricted denom. Restriction controls circulation; it does not protect the token from its own administrator.

The ante checks and their order

NFTSendCheckDecorator

Runs on every transaction. It walks nft.MsgSend, including messages nested inside an authz.MsgExec up to eight levels deep, and rejects locked, blacklisted or unknown-counterparty transfers.

ZeroBalanceCheckDecorator

Runs in CheckTx and in simulate only. It re-applies the send decision to every MsgSend after the rest of the ante chain, so a dry run shows the true result. It does not run in DeliverTx.

Income prepends its send restriction and the token factory appends its own, so the order on every bank send is fixed.

  • 1. income settles both sides against their pre-transfer balances
  • 2. tokenfactory applies the blacklist, then the restriction

Income must run first: a holder entitlement is a function of the balance they had before the transfer. Checking compliance first would mean settling a balance that no longer exists.