Insight

One function decides every send

Bank sends, NFT sends and swap settlement all reach the same function on CenterChain. This is what it checks, in what order, and what it deliberately does not check.

A bank send, an NFT send and a swap settlement all pass through one function on CenterChain. The first rule that matches decides. For a bank denom there is no second, weaker path, and there is no per-module override.

That single sentence is easy to state and easy to misread. This post is about what the function actually checks, and about the two things it is often described as doing that it does not do.

Source: docs/concepts/transfers-and-compliance.md:10-11, docs/concepts/overview.md:58-59, docs/modules/tokenfactory.md:89-91

The order of the decision

tokenfactory.CanSendTokens is consulted for bank sends, NFT sends and swap settlement alike. It walks a fixed order, and it stops at the first rule that matches:

  1. Is the recipient blacklisted for this denom? Reject.
  2. Is the sender blacklisted for this denom? Reject.
  3. Is the denom restricted? If not, allow.
  4. If it is restricted, is the sender the admin or a delegate, or is this an internal leg of a swap? Allow.
  5. If it is restricted and none of the above, does the recipient already hold the denom? Allow, otherwise reject.

The four outcomes at the end of that list are the entire surface. There is no scoring, no risk weighting, and no policy engine that can be tuned from outside.

Source: docs/concepts/transfers-and-compliance.md:13-29, docs/modules/tokenfactory.md:94-104

The blacklist names denoms, not addresses

A blacklist is a per-denom list of addresses that may not send or receive that denom. Being blocked on one token has no effect on any other token. A denom with no authority metadata is never subject to the check at all, so a plain SDK denom passes through untouched.

Only the denom admin can add or remove an entry, and an account cannot blacklist itself. Adding is idempotent, so adding the same address twice is not an error. Removing an entry 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, and it applies to admins, to delegates and to the module authority. There is no path that a privileged signer walks through because the rule is meant to hold everyone else back.

Source: docs/concepts/transfers-and-compliance.md:31-42, docs/modules/tokenfactory.md:67, 106-108, 125-127

Closing circulation without freezing holders

is_restricted is a per-denom flag set by the denom admin. When it is set, that denom can only be sent to an address that already holds a balance of it. The admin, the delegates and an internal swap leg are exempt.

This is the on-chain circulation control. An issuer can close a token to new addresses and leave existing holders able to trade among themselves. The rule is about where a denom may go, not about who currently owns it.

Source: docs/concepts/transfers-and-compliance.md:44-51, docs/modules/tokenfactory.md:51, 110-116

It is a balance check, not an identity check

The restricted-denom rule tests a balance. The chain can prove that a recipient already holds the token. It cannot prove who that recipient is.

So the rule is not a KYC list, and calling it one is wrong in both directions at once. It blocks nothing extra, because there is no extra to block: an address that already holds the denom passes whether anyone knows its owner. And it claims an assurance the chain cannot make, because no message in the modules carries a holder identity.

Source: docs/concepts/transfers-and-compliance.md:53-58, docs/start-here/introduction.md:57-58, docs/modules/tokenfactory.md:117-119

There is no separate account freeze

The chain has no account-level freeze mechanism. Three things exist, and they are easy to confuse:

  • A 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.
  • An income blocklist check: a claim is rejected outright for a blocklisted holder.

A transfer barrier is not immutability. The admin of a restricted denom can still burn, force transfer, or hand the admin role to another account. Restriction controls circulation; it does not protect the token from its own administrator.

Source: docs/concepts/transfers-and-compliance.md:98-110

The reservation is enforced below the module

An open swap authorization reserves max_from against the holder’s balance. That reservation is enforced at the store layer, by a wrapper around the bank key value store, not by a check inside one transfer handler.

The difference is the part worth remembering. Because the check sits under every write that lowers a balance, it holds whichever code path moved the funds: a pool withdrawal, a direct send, or a staking unbonding. A handler-level check would only cover the path that carries it.

Source: docs/concepts/transfers-and-compliance.md:73-82

Two decorators, and only one of them reaches consensus

Two ante decorators wrap the ante handler:

  • 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 simulation only. It re-applies CanSendTokens to every MsgSend after the rest of the ante chain, so a dry run shows the result a real transaction would produce.

The second one does not run in DeliverTx. It exists so that a simulation and a real transaction agree. It is not a second enforcement path.

Source: docs/concepts/transfers-and-compliance.md:60-71

Order matters with income

Income prepends its send restriction and tokenfactory appends its own, so the order on every bank send is fixed: income settles both sides against their pre-transfer balances first, then tokenfactory runs the blacklist and the restriction.

Income has to run first. A holder’s entitlement is a function of the balance they had before the transfer. Checking compliance first would mean settling against a balance that no longer exists.

Source: docs/concepts/transfers-and-compliance.md:84-96

Where to check this yourself

The decision tree, the blacklist rules and the reservation are documented, and the documentation is written from the code rather than from the intent. If a page says the chain does something you do not find here, the page is describing an intention, not the behaviour.

  • Transfers and compliance: docs/concepts/transfers-and-compliance.md
  • The decision engine in context: docs/concepts/overview.md:51-59
  • The module that owns the function: docs/modules/tokenfactory.md:87-119

The swap messages the reservation is written for are described in a swap outside a pool takes two transactions.