Rotate the compliance authority via governance
Question. Hkchain uses a single operational key — the compliance authority — to sign sanctions-list pushes, rule tuning, address blacklists, and address freezes. What happens when that key needs to rotate (planned hand-over, suspected compromise, HKMA-directed change)? Who authorises the change, and what prevents the compromised key from rotating itself?
Short answer. The chain separates two authorities.
Governance is the policy authority (the x/gov module account
address). The compliance authority is the operational authority
(x/compliance.Params.Authority — a single bech32 address).
Rotation is a governance-signed MsgUpdateParams on the
compliance module; the new authority address takes effect at the
block the proposal passes and the message executes. The operational
compliance authority cannot rotate itself — only governance can —
so a compromised key cannot lock in its compromise.
Why two separate authorities
Collapsing governance and the operational compliance authority into a single key would collapse an audit boundary the framework relies on. The two roles are materially different:
| Governance authority | Operational compliance authority | |
|---|---|---|
| Signs | Policy changes: rotations, param updates, denom registrations | Operational actions: sanctions pushes, rule tuning, blacklists, freezes |
| Speed | Deliberate — proposal, vote, execution | Immediate — a single signed tx |
| Held by | The chain's governance process (voting stakeholders in MVP; HKMA-acceptable multisig in finalProduct) | The issuer's compliance team (single key in MVP; multisig in production) |
| Accountability | Recorded as a gov proposal with voter trail | Recorded as direct-signed tx with authority address |
| What it must not do | Execute per-address enforcement actions | Change the rules the enforcement operates under |
Separation means the chain can distinguish "the policy changed" from "the compliance team acted." A single key doing both would erase that line in every on-chain event stream.
It also means key-compromise blast radius is bounded. A compromised compliance-authority key can (until detected) push rogue sanctions lists or tune rules; it cannot rotate itself, upgrade the module, or change governance. A governance-key compromise is a different, higher-severity incident addressed at the governance layer — outside the scope of this flow.
The rotation flow, step by step
0. Prerequisite: the current state
x/compliance.Params.Authority holds the current compliance
authority address. The module constructor also carries a separate
authority field — the governance authority used for
MsgUpdateParams. These two are distinct by design; the former
rotates via governance, the latter is the module's gov-module-
account address fixed at chain bootstrap.
x/kyc.msgServer.Blacklist — the keeper primitive called by every
compliance path that needs to write LEVEL_BLACKLISTED — reads the
current compliance authority at tx time through the
ComplianceKeeper expected-keeper interface:
ComplianceKeeper.GetComplianceAuthority(ctx) → Params.Authority
This lookup happens every tx, so rotations take effect immediately at the block they commit; no cached authority anywhere in the stack.
1. A governance proposal is submitted
The rotation starts with a standard governance proposal carrying
MsgUpdateParams on the compliance module:
MsgUpdateParams {
authority: gov_module_account_addr,
params: Params {
authority: new_compliance_auth_addr,
rules: <unchanged>
}
}
The proposal is authored off-chain, submitted via the usual
MsgSubmitProposal carrying the update as its first message, and
enters the voting period. Stakeholders vote; on passage, the
proposal's messages execute.
At execution time, MsgUpdateParams runs:
- Authority check:
msg.authoritymust equal the module's constructor-time governance address. Any other signer is rejected with an unauthorised-authority error. This is the check that enforces "only governance can rotate the compliance authority" — the existing compliance authority's signature is not accepted on this message, becauseauthorityis the gov module account, notParams.Authority. - Params replace: the new
Params(including the newAuthority) is written to the store. The old authority address is overwritten in place.
From the next block, every consumer of the compliance authority resolves it to the new address.
2. What the rotation affects immediately
The operational actions that gate on the compliance authority shift to the new key:
MsgUpdateSanctionsList— the sanctions-push path. The new key's signature passes the authority check; the old key's signature is rejected with the authority-mismatch error.MsgUpdateRule— the rule-tuning path (Rules.TravelRuleFullThreshold,VelocityWindowSeconds,VelocityTxLimit).x/kyc.msgServer.Blacklist— the single-address blacklist path consulted via theComplianceKeeperinterface. The new key is now the recognised signer.x/stablecoin.MsgFreezeAccount/MsgUnfreezeAccount— the address-freeze path, gated on the same compliance-authority address resolved via keeper.
All of these read the current authority at tx time; none of them holds a cached reference.
3. What the rotation does not affect
Existing on-chain state. Addresses already at
LEVEL_BLACKLISTED, already-frozen accounts, the current
SanctionsSet, prior AuditLog entries — all remain. Rotation
changes who is allowed to write to those structures; it does
not rewrite them.
Pending flags. Flags emitted before the rotation remain in their current status. Reviewers and officers act on flags through sub-role bindings under an L5 issuer, not through the compliance authority — so rotating the compliance authority does not affect the reviewer / officer workflow.
Governance authority. The module's constructor-time authority
field — the governance address permitted to sign
MsgUpdateParams — is unchanged. Rotating governance itself is a
different problem, solved by the normal governance-proposal
mechanics of the SDK's x/gov.
KYC-provider allowlist. x/kyc.Params.kyc_provider_addresses
(the distinct allowlist used by MsgEnroll / MsgUpgradeLevel)
is not the compliance authority. Rotating that list is a separate
MsgUpdateParams on x/kyc — a different governance proposal
with a different target.
The audit trail, end to end
For a rotation:
- Governance proposal recorded via
x/govwith theMsgUpdateParamsbody visible in the proposal details. Vote tally, voter addresses, and passage block are all public. - Proposal execution at the block the voting period closes.
The
MsgUpdateParamshandler runs; the new params are written. - Params event (or direct Params read) — the new
Params.Authorityis observable from the next block throughQueryParams. No dedicatedEventAuthorityRotatedis emitted in MVP — the governance proposal trail is the authoritative record.
A reviewer or regulator can reconstruct: which proposal rotated
the authority, when it passed, what address was rotated out, what
address was rotated in, and which stakeholders voted. The
separation also makes it easy to answer "when did key X last hold
compliance authority?" by walking backward through
MsgUpdateParams executions on the compliance module.
MVP: the audit trail relies on the standard x/gov proposal
history; there is no compliance-module-specific rotation event.
finalProduct may emit an EventComplianceAuthorityRotated for
easier downstream indexing — a small proto extension reserved for
a follow-up pass.
Edge cases and what happens
Current compliance authority tries to rotate itself
Rejected with an unauthorised-authority error. MsgUpdateParams
requires the governance authority (module constructor address),
not Params.Authority. The authority value under rotation cannot
produce a valid signature for its own rotation — the two keys are
in different checks.
Rotation to an invalid address
Rejected at params validation. The new Params.Authority is
checked as a well-formed bech32 address at proposal-execution
time; malformed values fail the governance message's own
validation.
Rotation to the gov module account
Mechanically possible — the chain does not prevent it — but operationally it would partially collapse the two-authority separation. Governance votes would be required for every sanctions push. Strongly discouraged as an operational choice, though not blocked as a primitive.
Rotation during a flag review in progress
Harmless. Flag review uses sub-role bindings (officer / reviewer), not the compliance authority. The operator finishing a review at the rotation block can proceed without any handler change.
Rotation mid-sanctions-push
If a sanctions-push tx is in the mempool signed by the old authority when the rotation commits, the push is rejected at the next block with the authority-mismatch error. The push has to be re-signed by the new authority. This is the intended behaviour — the rotation is a boundary, and no in-flight operational action crosses it.
The new authority address holds no HKD / has no other role
Supported. The compliance authority is a pure signing role — it does not need to hold tokens, does not need a KYC identity, does not need sub-role bindings. A fresh bech32 address generated for the role is a common operational choice.
Repeatedly rotating
Idempotent. Each MsgUpdateParams replaces the full Params
value; rotating A → B → A → B leaves the chain at B with three
audit-trail entries for the proposals. No accumulated state; no
quota on rotation frequency (beyond what governance timing imposes).
Hand-over protocol (off-chain, informed by the on-chain primitive)
The chain provides the primitive; the hand-over protocol around it is operational. A typical hand-over runs:
- New key generated and held in the intended production custody (multisig, HSM, etc.).
- Governance proposal drafted naming the new address, linked to off-chain documentation explaining the change (new personnel, custody migration, incident response).
- Voting period; stakeholders verify the off-chain documentation.
- Proposal passes; rotation commits.
- Old key is formally retired and archived; custody of the old key is documented for any post-incident audit.
None of steps 2, 4, or 5 is a chain primitive. The chain gates only the rotation event itself; everything else is the governance and operations layer's responsibility.
On-chain state summary
| State object | On rotation |
|---|---|
x/compliance.Params.Authority | replaced with new address |
x/compliance.Params.Rules | unchanged (unless the proposal also updates rules) |
| Module constructor governance authority | unchanged |
x/kyc.Params.kyc_provider_addresses | unchanged (separate allowlist) |
x/gov.Proposal[n] | rotation proposal recorded with the body visible |
x/kyc.Identity rows, SanctionsSet, AuditLog | unchanged |
| Reviewer / officer sub-role bindings | unchanged |