Mint HKD into circulation
Question. HSBC holds HKD reserves with the auditor's latest
attestation on file. How does that reserve become circulating
hkd.hsbc in a customer's wallet?
Short answer. MsgMint (creates supply against a fresh
attestation, into HSBC's treasury pot) → MsgDistribute (releases
from the pot to a named recipient). The chain treats the two steps
as separate accounting events: mint anchors new supply to a specific
attestation; distribute is the moment tokens enter circulation.
The flow, step by step
0. Prerequisite: an ATTESTED reserve attestation
Before minting, HSBC must have an attestation on chain that the
auditor has co-signed. The Proof-of-Reserve flow covers that lifecycle
— submission, auditor co-sign, finalization — and ends with an
attestation row in state ATTESTED, timestamped at the auditor's
co-sign block. That timestamp is what the mint step checks for
freshness.
1. HSBC signs MsgMint
MsgMint { issuer: hsbc, amount: 1_000_000 hkd.hsbc, attestation_ref: A }
On chain:
x/stablecoinresolves HSBC's identity inx/kyc, verifiesLevel = LEVEL_LICENSED_ISSUER(L5). Anyone not at L5 is rejected.x/stablecoinreads the latestATTESTEDattestation for HSBC fromx/reserve. IfblockTime − attestedAt > max_attestation_staleness(default 7 days), the mint is rejected with a stale-attestation error. This is the on-chain teeth behind "no unbacked supply."bankKeeper.MintCoins(stablecoin, 1_000_000 hkd.hsbc)— the newly created supply lands in thex/stablecoinmodule-account pot. HSBC does not yet hold these tokens in its own EOA.IssuerTreasury[hsbc][hkd.hsbc].minted_available += 1_000_000.EventMinted { issuer, amount, attestation_ref }is emitted.
At this moment:
- Total supply = +1,000,000
hkd.hsbc - Module-account pot holds +1,000,000
hkd.hsbc - HSBC's EOA balance = 0
hkd.hsbc - HSBC's treasury row:
minted_available = 1,000,000,pending_redemption = 0
The tokens exist, but they are not in circulation. In the dashboard this shows up as "In treasury," distinct from "In circulation."
2. HSBC signs MsgDistribute to an institutional customer
MsgDistribute { issuer: hsbc, recipient: acme_corp, amount: 500_000 hkd.hsbc }
On chain:
- Pause checks: global pause and per-issuer pause both must be off.
- Denom checks:
hkd.hsbcis registered, and the denom's recorded issuer matches the signing issuer (HSBC cannot distributehkd.boc). - Treasury check:
minted_available >= 500_000. bankKeeper.SendCoinsFromModuleToAccount(stablecoin, acme_corp, 500_000 hkd.hsbc)— the pot releases to the recipient.IssuerTreasury[hsbc][hkd.hsbc].minted_available -= 500_000.EventDistributed { issuer, recipient, amount }is emitted.
At this moment:
- Total supply = 1,000,000
hkd.hsbc(unchanged by distribute) - Module-account pot holds +500,000
hkd.hsbc(remaining treasury) acme_corpholds +500,000hkd.hsbc- HSBC's treasury row:
minted_available = 500,000
The distributed 500,000 is in circulation. acme_corp can now
transfer, re-sell, or redeem through the normal flows. The remaining
500,000 sits in HSBC's treasury pot until a future MsgDistribute.
Why two steps instead of one?
A single-step "mint-to-recipient" would have worked mechanically, but would fuse two distinct compliance events into one, and that has real cost:
Audit separation. Mint is an assertion about reserves ("I have reserves to back this supply"). Distribute is an assertion about counterparties ("I am placing this supply with this named holder"). These are different events from a compliance perspective and regulators will ask different questions about each. Keeping them separate lets each emit its own event and audit trail.
Observable separation of "issued" from "in circulation." Two supply quantities are kept as independently queryable chain state:
minted— supply created against reserves, whether or not yet placed with a holder.in circulation=minted − Σ treasuries— supply held by external counterparties.
A single-step flow would collapse these into the same number by construction. The two-step flow exposes "supply the issuer has committed to the protocol but not yet placed with a holder" as a first-class, independently readable value. This is the line between "issuer has capacity to serve demand" and "issuer has active customer exposure" — a distinction any downstream consumer (reports, operations consoles, supervisory views, wallets) can read directly from chain state without reconstruction.
Unwind safety. If a post-mint problem is discovered (attestation defect, reserve discrepancy found out-of-band), supply that is still in treasury can be burned back down without touching customer balances. Supply that has already been distributed cannot — it's in circulation and any unwind is a redemption. The two-step gives issuers an uncontroversial unwind window.
Treasury vs. circulation
The single most important distinction in the flow:
| Location | What it means | Who controls it |
|---|---|---|
Module-account pot (minted_available) | Minted, backing-anchored, not yet in circulation | HSBC, via MsgDistribute |
| Recipient EOA | In circulation, subject to normal transfer rules | Recipient |
Chain state exposes three independently queryable quantities for HSBC's denom:
- In treasury =
IssuerTreasury[hsbc][hkd.hsbc].minted_available - In HSBC's own EOA =
bank.GetBalance(hsbc_eoa, hkd.hsbc)(zero unless HSBC self-distributes — see below) - In circulation =
bank.GetSupply(hkd.hsbc) − Σ minted_availableacross all issuers of this denom
Edge cases and what happens
Mint against a stale or missing attestation
Rejected. If there is no ATTESTED attestation on file for HSBC, or
the latest one is older than max_attestation_staleness (default
7 days, governance-tunable), the mint fails. HSBC must get a fresh
attestation co-signed before it can mint again. This is enforced at
the chain layer; there is no way to bypass it.
Distribute more than the treasury holds
Rejected — insufficient treasury. HSBC cannot overcommit.
HSBC distributes to itself
Supported. MsgDistribute { recipient: hsbc_eoa } pulls supply from
the treasury pot into HSBC's own EOA, at which point it counts as
in-circulation (even though no external counterparty holds it yet).
Useful when HSBC wants to operate its own on-chain working balance.
Distribute to a blacklisted or frozen address
Inbound transfers to a blacklisted or frozen address are not blocked at the distribute step — blocking happens on outbound transfers. Operationally this does not matter: a blacklisted or frozen recipient cannot then spend the tokens they receive, so any distribution to such an address is effectively trapped. The policy question ("should we refuse to distribute to a known-frozen address?") is a product decision at the issuer-operations layer, not a chain-layer rule in the current design.
Multiple denoms from the same issuer
The model supports it — treasury rows are keyed by (issuer, denom).
MVP exercises the single-denom-per-issuer case (hkd.hsbc,
hkd.boc, etc., each owned by a different issuer). Multi-denom per
issuer (e.g. hkd.hsbc-retail and hkd.hsbc-wholesale coexisting
under HSBC) is a clean extension without schema change, typically
driven by product segmentation or regulatory tier requirements.
ERC-20 visibility
Any hkd.hsbc balance is simultaneously visible as the corresponding
ERC-20 wrapper balance on the EVM side — mint, distribute, transfer
all flow through the same supply. Wallets and dApps that only speak
ERC-20 see the distribution as a standard Transfer(0x0 → recipient)
event. This is a property of the Single Token Representation bridge,
not a separate flow.
On-chain state summary
| State object | After MsgMint | After MsgDistribute |
|---|---|---|
| Module-account balance | +N | −amount_distributed |
| Issuer EOA balance | unchanged (0) | unchanged unless self-distribute |
| Recipient EOA balance | — | +amount_distributed |
| Total supply | +N | unchanged |
IssuerTreasury.minted_available | +N | −amount_distributed |
IssuerTreasury.pending_redemption | unchanged | unchanged |
EventMinted | emitted | — |
EventDistributed | — | emitted |