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:
- Is the recipient blacklisted for this denom? Reject.
- Is the sender blacklisted for this denom? Reject.
- Is the denom restricted? If not, allow.
- If it is restricted, is the sender the admin or a delegate, or is this an internal leg of a swap? Allow.
- 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:
NFTSendCheckDecoratorruns on every transaction. It walksnft.MsgSend, including messages nested inside anauthz.MsgExecup to eight levels deep, and rejects locked, blacklisted or unknown-counterparty transfers.ZeroBalanceCheckDecoratorruns inCheckTxand in simulation only. It re-appliesCanSendTokensto everyMsgSendafter 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.