Hkchain

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. For scope: 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 = true is 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:

MessageEffect 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 (MsgSend from one EOA to another) go through x/bank, not x/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 of x/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 = true is 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 objectTouched by MsgPauseTouched by MsgUnpause
PauseState.Global (scope=GLOBAL)set trueset false
PauseState.Issuers[issuer] (scope=ISSUER)upserted paused=trueupserted paused=false
IssuerTreasuryunchangedunchanged
RedemptionRequest rowsunchanged (OPEN stays OPEN)unchanged
EventPauseChangedemitted (paused=true)emitted (paused=false)