Hkchain

hkchain.reserve.v1

The reserve module carries proof that circulating stablecoin is backed. It holds the two-signature attestation lifecycle, the auditor registry, and the daily par-value statement the chain writes for itself at each day boundary.

Schema: proto/hkchain/reserve/v1/{query,tx,events,reserve}.proto.

It never touches supply. Its one influence on issuance is indirect and important: x/stablecoin refuses to mint for an issuer whose latest attestation here has gone stale.

The attestation lifecycle

An attestation is a claim about one period — how much was circulating, how much reserve backed it, and the hash of the off-chain audit package that evidences both. It becomes trustworthy through two signatures from two parties.

StateValueReached by
ATTESTATION_STATE_PENDING_AUDIT2MsgSubmitAttestation — the issuer's claim, on the record but not yet attested
ATTESTATION_STATE_ATTESTED3MsgCoSignAttestation — a registered auditor co-signs. Terminal.

ATTESTATION_STATE_DRAFT (1) exists in the enum for a future off-chain drafting flow and is not reachable today: submission goes straight to pending. ATTESTATION_STATE_UNSPECIFIED (0) is not a lifecycle state — but you will meet it as a zero value, and one query returns it deliberately. See ReserveForAddress below.

Only ATTESTED counts. Every consumer — the mint freshness check, the daily statement, the public lookup — reads the latest attested row and ignores pending ones.

Queries

MethodGET pathParametersReturns
LatestAttestation/hkchain/reserve/v1/attestations/{issuer}/latestissuerLatest attested attestation; 404 if none
AttestationForPeriod/hkchain/reserve/v1/attestations/{issuer}/periodissuer, period_start, period_endThe attestation for exactly that period; 404 if none
ReserveForAddress/hkchain/reserve/v1/reserve_for_address/{address}addressbindings[] — one per held denom
DailyStatement/hkchain/reserve/v1/daily_statement/{issuer}issuer, dateThe statement for that UTC day; 404 if none
Auditors/hkchain/reserve/v1/auditorsinclude_revoked, paginationauditors[]
Attestations/hkchain/reserve/v1/attestations/{issuer}issuer, paginationattestations[], every state
Params/hkchain/reserve/v1/params—params

Note the path family: /attestations/{issuer} is the list, and /latest and /period hang off it.

Timestamp parameters are RFC 3339, not calendar dates. period_start, period_end and date are protobuf timestamps, so the gateway expects a full instant:

curl -s "$REST/hkchain/reserve/v1/daily_statement/hkchain1...?date=2026-08-09T00:00:00Z"

For date the handler truncates to the UTC day, so any instant inside the day resolves to that day's statement — but a bare 2026-08-09 does not parse.

LatestAttestation

The single most useful call in this module: it answers "is this issuer's backing current, and as of when".

curl -s "$REST/hkchain/reserve/v1/attestations/hkchain1.../latest"
{
  "attestation": {
    "id": "4",
    "issuer": "hkchain1...",
    "period_start": "2026-08-01T00:00:00Z",
    "period_end": "2026-08-08T00:00:00Z",
    "random_day": "2026-08-05T00:00:00Z",
    "circulation": { "denom": "hkd.hsbc", "amount": "125000000" },
    "reserve_market_value": { "denom": "hkd", "amount": "130000000" },
    "attestation_hash": "sha256:1f0a...",
    "issuer_sig": "",
    "auditor_sig": "...",
    "auditor": "hkchain1...",
    "state": "ATTESTATION_STATE_ATTESTED",
    "submitted_at": "2026-08-08T03:20:11Z",
    "attested_at": "2026-08-08T09:47:02Z"
  }
}

Two details in that payload:

  • Both denominations are issuer-supplied, and the chain does not check either. The intent is that circulation carries the issuer's stablecoin denom and reserve_market_value carries hkd, the off-chain currency — they are deliberately different, so never compare them as coins; compare the amounts with each denom's decimals in mind. But MsgSubmitAttestation validates only that both coins parse and are positive, so any syntactically valid denom is accepted and stored verbatim, and the daily statement copies the submitted reserve_market_value coin through unchanged. Read the denom on every coin before doing arithmetic on it, and reject the row in your own client if it is not the pair you expect.
  • random_day is the unannounced business-day snapshot the supervisory regime requires. It must fall inside the period, and the chain enforces that on submission — unlike the denominations.

404 means "no attested attestation", not "no such issuer." An issuer with a pending submission and nothing finalised answers 404 here.

AttestationForPeriod

Exact match on both boundaries — this is a lookup, not a range search. There is no "which attestation covers this date" query; ask for the period you already know. If two submissions carry the same period, the highest id wins.

curl -s "$REST/hkchain/reserve/v1/attestations/hkchain1.../period?period_start=2026-08-01T00:00:00Z&period_end=2026-08-08T00:00:00Z"

Unlike LatestAttestation, this one returns the row in whatever state it is in. Check state before presenting it as attested.

ReserveForAddress

The public verifiability endpoint: given any address, which reserve attestation covers the stablecoin it currently holds. One binding per registered denom the address holds a positive balance of.

curl -s "$REST/hkchain/reserve/v1/reserve_for_address/hkchain1..."
{
  "bindings": [
    {
      "issuer": "hkchain1...",
      "denom": "hkd.hsbc",
      "balance": { "denom": "hkd.hsbc", "amount": "25000" },
      "attestation": { "id": "4", "state": "ATTESTATION_STATE_ATTESTED", "…": "…" }
    }
  ]
}

Two answers look similar and mean different things:

  • bindings is empty — the address holds no registered stablecoin. Nothing to attest.
  • A binding whose attestation.state is ATTESTATION_STATE_UNSPECIFIED — the address holds the denom, but its issuer has no attested attestation. The attestation object is present and zero-valued rather than absent, because the field is not nullable.

Gate any "backed by" display on state == ATTESTATION_STATE_ATTESTED. Treating a zero-valued attestation as real would show a holder that their balance is backed by an attestation of zero.

Only denoms registered in x/stablecoin are considered; a native ahkc balance never appears here.

Auditors and Attestations

Both paginate in memory, by offset and limit, over a registry small enough that this is free. next_key is unused; page with pagination.offset, and send pagination.count_total=true if you want total populated.

Auditors hides revoked rows by default. Pass include_revoked=true to see the full history — revocation sets a timestamp rather than deleting the record, so the registry stays auditable.

Messages

MessageSigner fieldAuthorised when
MsgSubmitAttestationissuerSigner is LEVEL_LICENSED_ISSUER in x/kyc
MsgCoSignAttestationauditorSigner is in the auditor registry and not revoked
MsgRegisterAuditorauthoritySigner is the governance authority
MsgRevokeAuditorauthoritySigner is the governance authority
MsgUpdateParamsauthoritySigner is the governance authority

MsgSubmitAttestation — { issuer, period_start, period_end, random_day, circulation, reserve_mv, attestation_hash } → { attestation_id }.

Validation, in order: issuer level → period_end strictly after period_start → random_day inside [period_start, period_end) → both coins valid and positive → attestation_hash non-empty. The response carries the new id, which is what the auditor co-signs and what MsgMint references as attestation_ref.

"Valid and positive" is the whole coin check: the denominations are not validated against the issuer's registered denom or against hkd. Getting them right is the submitter's responsibility, and a consumer should verify them rather than assume them.

Nothing prevents an issuer from submitting overlapping or repeated periods. The chain records claims; it does not adjudicate between them.

MsgCoSignAttestation — { auditor, attestation_id, signature }.

The auditor must be registered and un-revoked, the attestation must exist and be in PENDING_AUDIT, and signature must be non-empty. On success the row moves to ATTESTED, recording the auditor address, the signature bytes and the block time.

Two properties worth designing around:

  • The signature bytes are stored, not verified. The chain records what the auditor supplied; it does not check it against a key. The trust comes from which registered account signed the transaction, which the SDK does verify.
  • The co-sign threshold parameter is not enforced. min_auditor_cosign_count exists in params and defaults to 1, but the handler finalises on the first valid co-signature regardless of its value. Raising it by governance would not today require a second auditor.

MsgRegisterAuditor — { authority, address, name, license_ref }. name must be non-empty; re-registering an existing address fails, including one that was revoked. MsgRevokeAuditor — { authority, address } — stamps revoked_at; revoking twice fails.

Typed events

EventEmitted byCarries
EventAttestationSubmittedMsgSubmitAttestationattestation_id, issuer, circulation, reserve_market_value, attestation_hash
EventAttestationCosignedMsgCoSignAttestationattestation_id, auditor
EventAttestationFinalizedMsgCoSignAttestationattestation_id, issuer
EventAuditorRegisteredMsgRegisterAuditoraddress, name, license_ref
EventAuditorRevokedMsgRevokeAuditoraddress
EventDailyStatementThe end-of-block hook, not a messageissuer, date, circulation, reserve_market_value, latest_attestation_id

A single co-sign emits both EventAttestationCosigned and EventAttestationFinalized, in that order and in the same transaction. The split anticipates a multi-signature threshold where the two would separate; an indexer today should key finalisation on the second and not assume the first implies a non-terminal state.

The daily statement

At the first block of a new UTC day the module writes one statement per issuer: circulation now, the reserve market value from that issuer's latest attested attestation, and the id of that attestation. It is written to state and emitted as an event, so history is queryable without replaying the event stream.

Three behaviours to know:

  • It has no transaction. EventDailyStatement is emitted from the block's end hook, so it carries no transaction hash and cannot be found by transaction search. Recover a missed one from the DailyStatement query, or from the block results at that height.
  • An issuer with no attested attestation is skipped entirely, and skipped without marking the day done — the module retries on the next block rather than publishing a statement with zero backing.
  • One row per issuer, not per denom. An issuer owning several denoms gets a single statement, and its circulation figure is taken from the first denom in store order. Read per-denom circulation from x/stablecoin instead of inferring it here.

date on the event is a plain YYYY-MM-DD string; date on the stored statement is a timestamp at UTC midnight. The event field is not a timestamp — do not parse the two the same way.

Parameters

ParameterTypeDefaultEffect
min_auditor_cosign_countuint321Declared threshold of auditor co-signatures. Validation rejects 0; the co-sign handler does not read it.

The parameter that actually governs this module's interaction with issuance lives elsewhere: max_attestation_staleness in hkchain.stablecoin.v1 decides how old the latest attested attestation may be before minting stops.

Errors

Codespace reserve; the code table is in Errors and rejections. The distinctive ones are 8 (attestation is in the wrong state for this transition — usually a second co-sign attempt), 9/11 (auditor not registered / already revoked) and 13 (invalid period — random_day outside the window is the common case).

Queries in this module answer with gRPC NotFound / HTTP 404 rather than module error codes.