Hkchain

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/stablecoin resolves HSBC's identity in x/kyc, verifies Level = LEVEL_LICENSED_ISSUER (L5). Anyone not at L5 is rejected.
  • x/stablecoin reads the latest ATTESTED attestation for HSBC from x/reserve. If blockTime − 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 the x/stablecoin module-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.hsbc is registered, and the denom's recorded issuer matches the signing issuer (HSBC cannot distribute hkd.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_corp holds +500,000 hkd.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:

LocationWhat it meansWho controls it
Module-account pot (minted_available)Minted, backing-anchored, not yet in circulationHSBC, via MsgDistribute
Recipient EOAIn circulation, subject to normal transfer rulesRecipient

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_available across 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 objectAfter MsgMintAfter MsgDistribute
Module-account balance+N−amount_distributed
Issuer EOA balanceunchanged (0)unchanged unless self-distribute
Recipient EOA balance—+amount_distributed
Total supply+Nunchanged
IssuerTreasury.minted_available+N−amount_distributed
IssuerTreasury.pending_redemptionunchangedunchanged
EventMintedemitted—
EventDistributed—emitted