Pause an issuer's hkd.* activity
Question. A compliance incident requires stopping all hkd.hsbc
supply operations immediately — new mints, new distributions, new
redemptions. How does that happen on chain, and what does it affect?
Short answer. MsgPause { scope: ISSUER, issuer: hsbc } signed by
HSBC itself. From the next block onward, the chain rejects every
MsgMint, MsgDistribute, MsgBurn, and MsgAckRedemption touching
hkd.hsbc until a matching MsgUnpause is submitted. A GLOBAL
pause, signed by governance, applies the same halt to every issuer's
denom in a single call.
The flow, step by step
1. HSBC signs MsgPause
MsgPause {
signer: hsbc,
scope: PAUSE_SCOPE_ISSUER,
issuer: hsbc,
reason: "incident-20260501-sample"
}
On chain:
- Authority check: for
scope: ISSUER, the signer must equal the issuer address — an issuer signs its own pause. Forscope: GLOBAL, the signer must be the governance authority. No other signer is accepted. - KYC check: the issuer address must be
LEVEL_LICENSED_ISSUER(L5). PauseState.Issuers[hsbc].paused = trueis written.EventPauseChanged { scope, issuer, paused: true, reason }is emitted.
At this moment: the chain carries a first-class record of the pause,
who signed it, when, and why. The reason string is plain text on
chain — whatever incident code the issuer's ops team uses — and lands
in the event record, timestamped at block height.
2. What halts
Any message whose effect is to change hkd.hsbc supply state is
rejected at message handling:
| Message | Effect when hkd.hsbc is paused |
|---|---|
MsgMint (HSBC) | Rejected. No new supply against any attestation. |
MsgDistribute (HSBC) | Rejected. No release from treasury into circulation. |
MsgBurn (any holder) | Rejected. No new redemption requests can open. |
MsgAckRedemption (HSBC) | Rejected. Existing open redemptions cannot close. |
The rejection carries a structured error that downstream tools (wallets, ops consoles) can surface as "paused by issuer" with the on-chain reason string.
3. In-flight redemption requests
Redemption requests already in OPEN state at the moment of pause
remain in place — they are not cancelled, and the parked tokens stay
in the module-account pot. They simply cannot be acked until
unpause. This is deliberate: cancelling in-flight requests during a
pause would either leave holders without their tokens (if tokens stay
parked) or re-credit holders with tokens the compliance team has
paused movement for (if tokens are returned). Holding the state
steady lets the incident resolve before settlement resumes.
After unpause, open requests can be acked normally. The opened_at
timestamp on each request remains the original burn time, so the
time-in-redemption metric continues to accrue across the pause
window — which is the right signal for SLAs and compliance review.
4. Scope boundary: what a pause does not halt
A pause gates the operations x/stablecoin processes directly. It
is not a generic freeze on all movement of the denom. Specifically:
- Plain token transfers between existing holders (
MsgSendfrom one EOA to another) go throughx/bank, notx/stablecoin, and are governed by the chain's compliance AnteHandler. Whether holder transfers of a paused denom are also halted is a decision of the compliance layer's transfer-gating policy, not ofx/stablecoin's pause flag. - ERC-20 wrapper transfers go through the same bank path as native transfers, so the same rule applies.
- Queries against
hkd.hsbc— balances, treasury rows, open redemption lists — remain available throughout the pause. Read paths are not gated.
Treat a pause as "the issuer cannot change supply." Full cross-cutting movement halts are the province of compliance enforcement, composed on top.
5. HSBC signs MsgUnpause
MsgUnpause { signer: hsbc, scope: PAUSE_SCOPE_ISSUER, issuer: hsbc }
On chain:
- Same authority rule: scope=ISSUER needs the issuer's signature; scope=GLOBAL needs governance.
PauseState.Issuers[hsbc].paused = false.EventPauseChanged { scope, issuer, paused: false }is emitted.
From the next block on, all four halted message types work again. In-flight redemption requests can be acked. Mints and distributes resume on the normal attestation-freshness basis.
6. Global pause
MsgPause { signer: gov, scope: PAUSE_SCOPE_GLOBAL, reason: "..." }
When scope: GLOBAL is set:
- Signer must be governance authority.
PauseState.Global = trueis written.- Global state supersedes per-issuer state. While a global pause is in force, every issuer's supply operations are halted regardless of their individual flag. An issuer that was already paused stays paused when global lifts; conversely, lifting global does not implicitly unpause a previously-paused issuer.
Governance can use this for market-wide incidents (a chain-level compliance event, coordinated investigation across issuers, regulatory directive). It is a blunt instrument — no issuer can service any customer operation — so the expected use is rare and time-bounded.
Why per-issuer and global as separate scopes?
One issuer's incident should not require halting the whole market, and a regulator-level incident should not require collecting per-issuer signatures. The two scopes map to two different incident classes with two different signing requirements:
- Per-issuer pause is the issuer's operational tool. The issuer holds their own signing key and can pause their own denom without going through governance. Response time is bounded only by tx inclusion.
- Global pause is governance's tool. The signing bar is higher (governance vote, or whatever signer policy governance authority uses), but the blast radius is commensurately larger.
Both leave distinct, queryable audit records — EventPauseChanged
carries the scope so that supervisory review can separate issuer
self-pauses from system-wide governance pauses.
Edge cases and what happens
Pause during an open redemption that was about to be acked
The ack is rejected. The request stays OPEN with the original
opened_at. After unpause, the issuer can ack normally; the fiat
reference lands on the original request row as usual.
Pause from a non-issuer signer for scope=ISSUER
Rejected with an unauthorized-authority error. Only the issuer itself can pause its own denom. There is no "pause on behalf of" surface in the current design.
Pausing an already-paused denom
Idempotent. The state write is the same (paused=true); another
EventPauseChanged is emitted with paused=true. No error. This
lets ops tooling issue "pause" defensively without first reading
state.
Unpausing while global is in force
The per-issuer flag flips to paused=false, but supply operations
still do not work — because global supersedes. Once global lifts,
operations resume. Unpausing per-issuer during a global pause is
still a valid operation; it simply has no immediate effect.
Multiple issuers
Each issuer's pause flag is independent. Pausing hkd.hsbc does not
affect hkd.boc. One issuer's incident does not propagate across
the market — this is a load-bearing property for a multi-issuer
design.
On-chain state summary
| State object | Touched by MsgPause | Touched by MsgUnpause |
|---|---|---|
PauseState.Global (scope=GLOBAL) | set true | set false |
PauseState.Issuers[issuer] (scope=ISSUER) | upserted paused=true | upserted paused=false |
IssuerTreasury | unchanged | unchanged |
RedemptionRequest rows | unchanged (OPEN stays OPEN) | unchanged |
EventPauseChanged | emitted (paused=true) | emitted (paused=false) |