Skip to main content
Version: 0.1.0

Token factory

Module name and store key: tokenfactory. Consensus version 2.

x/tokenfactory is the base layer of CenterChain. It lets any account create its own bank-native denom, administer it, and decide who may hold or receive it. It also implements the two-phase swap authorization, which is the only way to move one CenterChain denom into another outside a liquidity pool.

The denom model​

A denom is created by the account that will administer it:

factory/{creator}/{subdenom}

creator is the signer's bech32 account address, and the creator becomes the denom's admin. The full string is then validated by sdk.ValidateDenom, so it behaves as an ordinary bank denom everywhere else.

LimitValue
Subdenom length44 bytes
Creator segment length75 bytes
Reserved namespacefactory/liquidity/pool/{pool_id}

The liquidity namespace is reserved. A genesis file may only contain such a denom if the liquidity module account administers it, and it must match a registered pool id with a non-zero id. That is what stops an account from minting a counterfeit LP share.

Creating a denom writes three things: bank denom metadata, a DenomAuthorityMetadata record, and a creator-to-denom index entry.

Per-denom administration​

DenomAuthorityMetadata is the whole administrative surface of a denom.

FieldMeaning
adminThe account that may mint, burn, and change policy. The creator at creation.
delegatesAccounts that bypass the restricted-denom rule for this denom.
is_restrictedWhen set, the denom may only be sent to an address that already holds it.

Change the admin with MsgChangeAdmin. It is a single step and emits an event; the previous admin cannot block it.

Messages​

MessageSignerRequires
MsgCreateDenomsenderNothing; the creator becomes the admin. Pays the creation fee and gas.
MsgMintsenderDenom admin, or the module authority through the sudo-mint capability.
MsgBurnsenderDenom admin. Burning from another address needs enable_burn_from.
MsgForceTransfersenderDenom admin and enable_force_transfer.
MsgChangeAdminsenderDenom admin.
MsgSetDenomMetadatasenderDenom admin and enable_metadata; the module authority may also set LP denom metadata.
MsgAddDelegate / MsgRemoveDelegatesenderDenom admin.
MsgAddBlacklist / MsgRemoveBlacklistsenderDenom admin. An account cannot blacklist itself.
MsgSetDenomRestrictionsenderDenom admin.
MsgSetPoolablesenderDenom admin.
MsgSetPoolDenylistauthorityThe module authority, which is the gov account by default.
MsgUpdateParamsauthorityThe module authority.

Capabilities are a module-wide allow-list, configured at wiring time. The shipped chain enables enable_metadata, enable_force_transfer, enable_burn_from, enable_admin_sudo_mint, and enable_community_pool_fee_funding. A capability that is absent disables the message; the check is an exact string match.

Params​

FieldDefaultNotes
denom_creation_fee10000000ucenCharged at denom creation, and burned unless community-pool funding is enabled.
denom_creation_gas_consume2000000Extra gas consumed by the create-denom path, so creation costs more than a normal message.

Only denom_creation_fee is validated.

Compliance: the send restriction​

tokenfactory installs itself as a bank send restriction through bankKeeper.AppendSendRestriction, and it separately owns the NFT transfer policy. One function decides every send.

CanSendTokens(ctx, from, to, coins):

for each coin:
1. recipient blacklisted for this denom -> ErrRecipientLocked
2. sender blacklisted for this denom -> ErrSenderLocked
3. denom has authority metadata and is_restricted:
sender is the admin or a delegate -> allowed
the coin is a swap leg -> allowed
recipient already holds the denom -> allowed
otherwise -> ErrRestrictedSender

Denoms with no authority metadata pass through untouched, so a plain SDK denom is never affected by this logic. The blacklist is per denom: being blocked on one token does not block another.

The restricted-denom rule​

is_restricted is a per-denom, on-chain allow-list expressed as a balance check. Under RESTRICTED, an address can only receive the denom from an admin or a delegate, or in a transfer from somebody who already holds it. This is the mechanism that lets an issuer close circulation to new addresses without freezing existing holders.

It is deliberately not a KYC list. The chain knows that a recipient holds a balance; it cannot know who that recipient is.

State limits​

LimitValueEnforced when
Factory denoms100,000Denom creation.
Pool policy records100,000First MsgSetPoolable and first MsgSetPoolDenylist.
Blacklisted addresses100,000Globally, across all denoms.

The counters are seeded by a consensus migration from version 1 to 2, and ExportGenesis fails if any collection exceeds its cap.

Pool eligibility​

A denom can only enter a liquidity pool if it is poolable. Non-tokenfactory denoms — the native coin, and assets arriving over IBC — are eligible unless the governance denylist says otherwise. A tokenfactory denom must additionally have bank metadata, a non-empty admin, and its poolable flag set.

The liquidity module sets poolable = true automatically for the LP denom it creates, and clears it when a pool is retired.

The swap authorization​

The module implements a two-phase swap: the holder grants a limited, single-use, time-boxed permission, and a named executor settles against it later.

PhaseMessageSigner
TX1MsgCreateAuthorizationholder
TX2MsgExecuteSwapexecutor
CancelMsgRevokeAuthorizationholder

Configuration of the executor list is governance-only, through MsgSetSwapConfig with a SwapConfig holding the technical issuer and the executor addresses.

Authorization terms​

MsgCreateAuthorization {
holder // granter, and the only destination 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 // fungible input, empty for an NFT input
to_denom // fungible output, empty for an NFT output
to_amount // exact amount the holder receives
max_from // ceiling on the total input
gas_fee_max // ceiling on the gas reimbursement
from_nft // NFT input: class_id + token_id
to_nft // NFT output: class_id (token id is assigned at settlement)
}

ValidateBasic rejects the message unless the shape is exact:

  • Both addresses must be canonical bech32, and holder != executor.
  • order_seq != 0, and chain_id is 1 to 128 characters.
  • At most one NFT scope. Fungible-to-fungible requires two distinct valid denoms.
  • All amounts are canonical decimal strings: v.String() == s.
  • max_from must be positive, and gas_fee_max must be strictly below it, so the gas ceiling can never eat the whole swap budget.
  • An NFT input pins to_amount and max_from to 1 and forbids a positive gas fee. An NFT output pins to_amount to 1 and allows a gas fee.

Creating an authorization​

The keeper validates in a fixed order:

  1. chain_id must equal the current chain.
  2. order_seq must be greater than the holder's high-water mark. Sequence numbers are never reused, so a delayed TX2 cannot consume a fresh authorization with a stale id.
  3. The holder must not be a module account.
  4. Neither leg may be blacklisted.
  5. The holder must not be locked for the input.
  6. An NFT input requires that the holder owns the token and that it is not already locked; the token is locked under the order id.
  7. A fungible input reserves max_from. The holder's spendable balance must cover every reservation, open orders included.

The per-holder high-water mark is stored next to the authorization, and the reservation is a counter per (holder, denom).

Executing a swap​

MsgExecuteSwap carries executor, holder, order_seq, chain_id, used_from, to_amount, and gas_fee. Validation:

  1. Canonical addresses, then the chain domain.
  2. The authorization must exist and not be consumed, revoked, or bound to a different executor.
  3. The budget: used_from <= max_from, gas_fee <= gas_fee_max, gas_fee < used_from, and to_amount equal to the authorized amount.
  4. The input leg is re-checked through CanSendTokens, so a blacklist entry or a restriction change between the phases blocks settlement.

Settlement, in one atomic transaction:

  • An NFT input transfers the escrowed token to the executor and mints the requested output.
  • A fungible input releases the reservation, pays gas_fee in the input denom to the executor, sends the remainder to the module, and burns it if the executor is the input denom's admin. The output is then minted to the holder.

consumed, used_from, and gas_fee are written in the same transaction, so a retried TX2 fails.

Revoking​

MsgRevokeAuthorization takes holder, order_seq, and chain_id. It rejects an authorization that is already consumed or revoked, releases the balance reservation or unlocks the NFT, and sets the revoked flag. Consumption and revocation are mutually exclusive: whichever lands first wins, and both are evented.

Gas​

The gas fee is paid in the input denom by the holder, not in ucen. The reimbursement mode is fixed and reported by the query as input_asset_reimbursement. The holder authorizes the ceiling with gas_fee_max; the executor charges the actual amount.

Queries​

QueryReturns
ParamsThe module parameters.
DenomAuthorityMetadataThe admin, delegates, and restriction flag for one denom.
DenomsFromCreatorEvery denom created by an address.
DenomsFromAdminEvery denom an address currently administers.
DenomBlacklistPaginated blacklisted addresses for a denom.
AuthorizationOne authorization, its high-water mark, the reserved amount, and the gas mode.

The swap query service has no HTTP annotation; reach it over gRPC only.

Events​

EventAttributes
create_denomcreator, new_token_denom
tf_mintmint_to_address, amount
tf_burnburn_from_address, amount
force_transfertransfer_from_address, transfer_to_address, amount
change_admindenom, new_admin
set_denom_metadatadenom, denom_metadata
set_denom_restrictiondenom, is_restricted
set_poolabledenom, poolable
set_pool_denylistdenom, denylisted
authorization_createdorder_id, holder, executor, used_from, gas_fee, to_amount, gas_mode
authorization_revokedsame shape
swap_executedsame shape, plus the NFT scope fields when set

Blacklist, delegate, and params changes emit no event. Watch chain state for those, not the event stream.

Errors​

CodeIdentifierRaised when
1100ErrInvalidSignerA governance message was not signed by the authority.
1102ErrDenomExistsThe denom already has bank metadata.
1103ErrUnauthorizedThe signer is not the denom admin, or the authorization is unavailable.
1104ErrInvalidDenomThe denom does not deconstruct to factory/....
1105ErrInvalidCreatorThe creator segment is empty or contains a separator.
1107ErrInvalidGenesisThe genesis state is inconsistent.
1108ErrSubdenomTooLongThe subdenom exceeds 44 bytes.
1109ErrCreatorTooLongThe creator segment exceeds 75 bytes.
1110ErrDenomDoesNotExistMinting or burning an unknown denom.
1111ErrCapabilityNotEnabledThe required capability is not configured.
1112ErrRestrictedSenderA restricted denom was sent to a non-holder.
1113ErrSenderLockedThe sender is blacklisted for the denom.
1114ErrRecipientLockedThe recipient is blacklisted for the denom.
1115ErrLiquidityModuleThe liquidity module is not registered with the factory.
1116ErrLiquidityDenomAn LP denom does not match its pool.
1117ErrStateLimitA state collection is at its cap.

The swap subsystem additionally uses plain errors with stable text: wrong chain domain, order sequence already used, module holder not allowed, insufficient unreserved funds, authorization unavailable, authorization budget exceeded, and authorization terminal.

CLI​

centerchaind query tokenfactory params
centerchaind query tokenfactory denom-authority-metadata <denom>
centerchaind query tokenfactory denoms-from-creator <creator>
centerchaind query tokenfactory denoms-from-admin <admin>
centerchaind query tokenfactory denom-blacklist <denom>

centerchaind tx tokenfactory create-denom <subdenom>
centerchaind tx tokenfactory change-admin <denom> <new-admin>
centerchaind tx tokenfactory add-delegate <denom> <address>
centerchaind tx tokenfactory remove-delegate <denom> <address>
centerchaind tx tokenfactory add-blacklist <denom> <address>
centerchaind tx tokenfactory remove-blacklist <denom> <address>
centerchaind tx tokenfactory set-denom-restriction <denom> <bool>
centerchaind tx tokenfactory set-poolable <denom> <bool>
centerchaind tx tokenfactory set-pool-denylist <denom> <bool>

centerchaind tx tokenfactory mint <amount>
centerchaind tx tokenfactory mint-to <address> <amount>
centerchaind tx tokenfactory burn <amount>
centerchaind tx tokenfactory burn-from <address> <amount>
centerchaind tx tokenfactory force-transfer <amount> <from> <to>
centerchaind tx tokenfactory modify-metadata <denom> <symbol> <description> <exponent>

The swap messages have no autocli entry. Build them as JSON and submit them with centerchaind tx broadcast or a gRPC client.