Freeze an address on compliance action
Question. A sanctions update, court order, or supervisory
directive requires blocking a specific address — say a holder of
hkd.hsbc — from moving tokens. How does that happen on chain, who
signs, and what does it actually prevent?
Short answer. MsgFreezeAccount { address } signed by the
compliance authority (not governance, not the issuer). The
address is marked frozen in x/stablecoin. Frozen addresses cannot
initiate redemption, and the compliance layer blocks their outbound
transfers (see the MVP note under "What the freeze prevents").
MsgUnfreezeAccount reverses the marker.
The flow, step by step
0. Prerequisite: who is the compliance authority?
The chain keeps a single compliance-authority address as a parameter
of the compliance module. It is the only signer authorized for
compliance actions — address freezes, sanctions-list updates, flag
resolution. Governance rotates this address through
MsgUpdateParams on the compliance module; day-to-day compliance
actions do not need a governance vote per operation.
Keeping this separate from the governance authority is deliberate. Governance signs policy changes; compliance signs operational actions against individual addresses. A single key with both roles would collapse an important audit boundary — the chain would lose the ability to distinguish "policy decision" from "enforcement action."
1. Compliance signs MsgFreezeAccount
MsgFreezeAccount {
authority: compliance_auth_addr,
address: alice_addr,
reason: "ofac-sdn-list-20260501",
evidence_hash: "sha256:3f8a..."
}
On chain:
- Authority check:
authoritymust equal the compliance authority resolved from the compliance module's parameters at tx time. Any other signer is rejected. - Address validation:
addressmust be a well-formed bech32 address. The chain does not require the address to have any existinghkd.*balance or prior activity — an address can be frozen preemptively (e.g. before the holder acquires any tokens, as part of a sanctions-list push). - A frozen marker is written at
FrozenAccounts[alice_addr]. EventAccountFrozen { address, frozen: true, reason, evidence_hash }is emitted.
The reason string is a plain-text code identifying the compliance
basis (sanctions list reference, case ID, directive number). The
evidence_hash is the SHA-256 of the off-chain case file, anchored
on chain so that a future reviewer can verify the on-chain action
against the exact document that authorized it. The file itself
stays off-chain (regulators and issuers keep sensitive case
material off-ledger); the hash gives cryptographic binding.
2. What the freeze prevents
Once the marker is in place, two things change for that address:
Cannot initiate redemption. A frozen address cannot submit
MsgBurn. The chain rejects the tx at handler time, regardless of
which hkd.* denom is involved. Redemption is a privileged
operation — it parks supply and opens a settlement obligation on
the issuer — so blocking it at the address level removes that
obligation from the frozen holder entirely.
Cannot transfer tokens out. Plain token transfers from the
frozen address (via x/bank, whether signed natively or through
the ERC-20 interface on the EVM side) are rejected before the tx
reaches supply processing. The frozen address can hold hkd.* but
cannot send it.
MVP: the freeze marker is currently consulted on the redemption path only, so
MsgBurnis refused but an ordinary transfer out is not. The primitive that stops an address transacting today is blacklisting — see Blacklist an address, which the sanctions and flag-review paths both reach. A compliance action that must halt all outbound movement should blacklist, or blacklist in addition to freezing.
The cumulative effect in the finalProduct form: tokens held by a frozen address are on-chain but inert. They exist in balance queries, they count toward total supply, they can be observed by auditors — but they cannot move until unfrozen.
3. What the freeze does not prevent
Inbound transfers. A frozen address can still receive
hkd.* tokens. This is intentional:
- The frozen address cannot spend what it receives anyway, so inbound is operationally a no-op for the frozen holder.
- Blocking inbound would force senders (and the issuer's distribute flow) to check recipient-freeze state on every transfer, which creates a different class of bug: transfers that inexplicably fail because a counterparty was frozen mid-flight, with no remedy for the sender.
- For sanctions semantics, receipt-without-spend matches the real-world model: frozen bank accounts can still receive wire deposits; the account holder just cannot use them.
If a specific policy requires blocking all inbound as well (e.g.
a total-quarantine action), that is a separate operational decision
layered on top — the issuer can refuse to MsgDistribute to the
frozen address, other senders can choose not to transfer. The chain
does not impose it as a primitive.
Redemption already in flight. If the frozen holder had an OPEN
RedemptionRequest at the moment of freeze, the request remains in
place. The issuer may still ack the redemption (settling off-chain
to the holder's registered bank account under whatever rules the
issuer's compliance team applies). Freezing the on-chain address
does not retroactively void on-chain obligations the issuer has
already committed to settle.
Queries and audit access. The frozen address's balances, history,
and any associated RedemptionRequest rows remain queryable. Freeze
is an action on movement, not visibility.
4. Compliance signs MsgUnfreezeAccount
MsgUnfreezeAccount {
authority: compliance_auth_addr,
address: alice_addr,
evidence_hash: "sha256:9c2b..."
}
On chain:
- Same authority check.
- Frozen marker is deleted.
EventAccountFrozen { address, frozen: false, evidence_hash }is emitted — same event type, negative value.
reason is not required on unfreeze (the positive action's reason
is already on chain); evidence_hash typically points to the
off-chain decision to lift the freeze (investigation closed,
sanctions list update, court order rescinded).
From the next block, the address can burn and send again. Tokens that were held during the freeze remain where they were — unfreeze does not forfeit or redistribute them; it simply removes the movement block.
The audit trail, end to end
For any freeze-unfreeze cycle on a single address, the chain preserves a structured audit record:
EventAccountFrozen { frozen: true, reason, evidence_hash }at block N.- (During the freeze) an attempted
MsgBurnfrom the address is rejected with a freeze error — as is an outbound transfer, in the finalProduct form noted above; those rejected txs are still part of the chain's mempool/rejection history, observable for supervisory review. EventAccountFrozen { frozen: false, evidence_hash }at block M.
A compliance review after the fact can reconstruct: who signed,
when, on what evidence, what the freeze prevented (if anything was
attempted), and who released it. The two evidence_hash fields
bind the on-chain actions to their off-chain authorization documents.
Edge cases and what happens
Freeze on a address with no tokens
Supported. The marker is written regardless of balance. Useful for preemptive sanctions-list propagation — the freeze is in place before any hypothetical future token movement.
Freeze from a non-compliance signer
Rejected with an unauthorized-authority error. Governance cannot
sign MsgFreezeAccount directly (governance's role is to rotate
the compliance authority, not to act as one). An issuer cannot
freeze an address either — freeze is a chain-wide compliance
action, not an issuer-scoped one.
Frozen address attempts self-transfer
Rejected in the finalProduct form: self-transfer (sending to the same address) still counts as outbound for the freeze check. Under the MVP scope noted above, the transfer path is not freeze-checked at all, so a self-transfer succeeds.
Freezing the compliance authority itself
Mechanically possible — nothing in the chain prevents it — but it is an operational foot-gun. Governance should rotate the compliance authority before a freeze lands on the old address; otherwise the frozen compliance authority can no longer sign the unfreeze.
External sanctions feed integration
The chain carries the freeze primitive; the upstream source of "which addresses to freeze" is orthogonal. A live sanctions-feed integration (automated push from OFAC, HKMA's own list, a chain analytics provider) is an external service layered on top of the compliance authority's signing key — not a chain feature. MVP includes manual freeze by the compliance authority; automated upstream feeds are a future integration, out of MVP scope.
Multiple freezes on the same address
The marker is idempotent. Re-issuing MsgFreezeAccount on a
frozen address re-emits the event with the latest reason and
evidence_hash — useful for attaching updated case material — but
does not double-lock anything. A single MsgUnfreezeAccount lifts
it regardless of how many freezes were issued.
On-chain state summary
| State object | Touched by MsgFreezeAccount | Touched by MsgUnfreezeAccount |
|---|---|---|
FrozenAccounts[address] | marker written | marker deleted |
| Bank balance of frozen address | unchanged | unchanged |
RedemptionRequest rows involving the address | unchanged | unchanged |
EventAccountFrozen | emitted (frozen: true) | emitted (frozen: false) |