Errors and rejections
A Cosmos transaction can fail in four places, and telling them apart is the whole job: each has a different cause, a different remedy, and a different effect on your account.
| Where | Surfaces as | Account sequence | What it means |
|---|---|---|---|
| Admission — generic | Non-zero code on the broadcast response | Unchanged | Malformed, badly signed, underfunded, wrong chain ID |
| Admission — compliance gate | Non-zero code on the broadcast response, codespace: "compliance" | Unchanged | A gate rejected the transfer. Never entered a block. |
| Block execution — pre-execution checks | Non-zero code on the transaction query, often codespace: "compliance" | Unchanged | Admitted, then rejected against the block's state. In a block, but nothing ran |
| Message execution | Non-zero code on the transaction query | Advanced | Included in a block and failed while running |
The first three cost nothing but the round trip and leave the sequence you signed at still valid. Only the last consumed gas and burned a sequence number.
The distinction that catches people is between rows three and four, because
both surface on the transaction query. Row three means the transaction was
admitted and the world changed underneath it — the recipient was blacklisted,
the issuer paused — so it can be re-signed at the same sequence once the cause
is addressed. Row four means your message ran and failed. Read codespace: a
compliance codespace on a transaction query is row three.
The EVM path fails differently; "EVM-side failures" at the end of this page covers it.
Reading a rejection
Two fields carry the answer:
codespacenames the module that produced the error.raw_logcarries the readable message, already assembled: the offending address, amount or mismatch, followed by the error's registered text.
recipient=hkchain1abc…: address is blacklisted
payload tier="full" does not match sender's KYC-registered tier="minimal" (level=LEVEL_RETAIL_BASIC)
code is a per-module number and is only meaningful with its codespace. Match
on the pair if you must branch in code; prefer matching on nothing and
surfacing raw_log.
Compliance gate rejections
Five gates can reject. Each maps to one registered error in the compliance
codespace:
| Code | Error | Raised by | Typical cause |
|---|---|---|---|
| 18 | address is blacklisted | kyc_gate | Sender or recipient blacklisted. The message names which. |
| 19 | counterparty is sanctioned | sanctions | A party's VASP is on the sanctions list |
| 20 | travel rule requirements not met | travel_rule | Missing payload, wrong tier, incomplete fields, or a mismatch against the identity registry |
| 21 | sub-role signer not allowed to sign this message type | role_gate | A delegated key signed outside its allowlist |
| 13 / 11 / 14 | invalid agent policy envelope / sub-role binding revoked / agent policy not found | agent_policy | Over a cap, delegation revoked, or an agent key with no policy attached |
Diagnosing a Travel Rule rejection
Code 20 covers several distinct failures. The message distinguishes them:
| Message contains | Fix |
|---|---|
travel rule payload not found | The payload was not attached, or was attached as something other than an extension option |
payload tier=… does not match sender's KYC-registered tier=… | State the tier your level is registered with, not the one the amount seems to need |
cannot originate transfer requiring tier=… | The sender's level is too low for this amount. Nothing about the payload will fix it. |
originator_ref and beneficiary_ref are required | Both refs must be present at any tier |
full tier requires originator_vasp and beneficiary_vasp | A full-tier sender must populate both VASP identifiers |
…_vasp=… does not match KYC-registered VASP … | The identifier disagrees with the identity record |
minimal-tier …_ref does not match KYC ivms101_off_chain_ref | At minimal tier the refs are pinned to the registry, not per-transfer |
For a transfer signed by a delegated key, every one of these is evaluated against the parent account. A payload describing the agent will fail the registry checks.
The gate trace
When raw_log is not enough, ask for the gate-by-gate record:
curl -s "$REST/hkchain/compliance/v1/tx_trace/<TXHASH>"
Each row is one gate: its name, BLOCKING or ADVISORY, a status, a reason,
and gas consumed. Statuses are PASS, FAIL, FLAGGED. The top-level
accepted is true only when every blocking gate passed — an advisory FLAGGED
does not flip it.
Reasons are written for a compliance reader, with a stable prefix per gate:
| Gate | Prefix on failure |
|---|---|
kyc_gate | KYC gate rejected: … |
sanctions | Sanctions screening rejected: … |
travel_rule | Travel Rule check rejected: … |
role_gate | Role-gate rejected: … |
agent_policy | Agent policy rejected: … |
Advisory gates carry a fixed label instead — Reviewer flag: transfer at or above HKD 8,000 threshold, Reviewer flag: transfer-density burst inside rolling window. A FLAGGED row means a human will look at the transfer, not
that anything failed.
A rejected transaction still has a trace: the rows up to and including the failure are recorded before the error propagates.
When the trace is gone
Traces are held in memory on the node that ran the transaction — 256 entries, one hour, whichever comes first — and are not chain state. When the lookup misses, re-run the same bytes through simulation:
curl -s -X POST "$REST/hkchain/compliance/v1/simulate_trace" \
-H 'Content-Type: application/json' \
-d '{"tx_bytes":"<base64 signed tx>"}'
The response has the same shape with simulated: true. It runs against
current state on a discarded copy, so it answers "what would happen now",
which is the same answer as long as nothing relevant changed.
Simulation is also the right pre-flight check: run it before broadcasting and a rejection costs you no round trip at all. Two limits on it:
- It decodes a Cosmos transaction. The bytes are decoded with the chain's
transaction decoder and the gates run over the messages found inside. It is
not an EVM call simulator: pointing it at an Ethereum transaction does not
execute the call, so it cannot report on a transfer made through the ERC-20
interface. The pre-flight check for that path is a read-only
eth_callof the same method with the same arguments, which runs the gates and returns the revert reason without submitting anything. - It requires the module's simulation hook to be wired at node start-up. A node that has not wired it answers with a configuration error rather than a trace.
Module errors
Errors raised while a message executes. These are execution failures — the transaction is in a block.
stablecoin
| Code | Meaning |
|---|---|
| 2, 3, 4 | Denom invalid, not registered, or already registered |
| 5, 6 | Signer is not the governance authority / not the denom's issuer |
| 7 | Reserve attestation stale or missing — minting requires fresh backing |
| 8 | Issuer activity is paused |
| 9 | Issuer treasury has insufficient minted-but-undistributed balance |
| 10, 11 | Invalid amount / address |
| 12, 13 | Redemption request not found / not open |
| 14 | Account frozen |
| 15, 16 | Invalid params / invalid pause scope |
kyc
| Code | Meaning |
|---|---|
| 2, 3 | Identity not found / already exists |
| 4 | Invalid level |
| 5, 6 | Signer is not an authorised identity provider / not the governance authority |
| 7 | Invalid address |
| 8 | Invalid level transition |
reserve
| Code | Meaning |
|---|---|
| 2, 3, 4, 5 | Invalid address / params, unauthorised authority / issuer |
| 6, 7, 8 | Attestation payload invalid, not found, or in the wrong state for this transition |
| 9, 10, 11 | Auditor not registered / already registered / revoked |
| 12, 13 | Invalid coin / invalid period |
compliance — beyond the gate errors above
| Code | Meaning |
|---|---|
| 2, 3, 4, 5 | Invalid address / params / authority / rules |
| 6, 7, 8 | Flag not found, wrong status for this transition, invalid decision |
| 9, 10, 11, 12 | Sub-role binding exists / not found / revoked / invalid role |
| 13, 14 | Invalid agent policy envelope / policy not found |
| 15, 16 | Signer is not a bound reviewer / officer |
| 17 | Travel Rule payload not found |
| 22, 23 | Simulation not configured at node start-up / transaction bytes failed to decode |
Sequence collisions
Two transactions signed at the same sequence: the first is admitted, the second is rejected. This does carry a code, and knowing which one saves an investigation:
| Field | Value |
|---|---|
codespace | sdk |
code | 32 |
raw_log | account sequence mismatch, expected N, got M: incorrect account sequence |
It arrives from the signature-verification stage, so a client that classifies
by stage rather than by code reports it as a signature problem — which is
misleading, because the signature was fine and the sequence was not. Branch on
sdk/32 and read the expected and received numbers out of raw_log; they
tell you exactly how far your local counter has drifted.
Track sequence locally when you submit in parallel; the account query reflects committed state and lags anything you have already broadcast.
The failure with no code at all
A transaction that never lands. The broadcast returned 0, so it was
admitted somewhere, but the hash does not resolve and no error is ever
produced. There is nothing to read: an admitted transaction can sit in a
mempool indefinitely, and it can equally be dropped from one — evicted when the
mempool fills, aged out, or lost when the node restarts — without notifying
anyone.
Treat this as a state your integration owns rather than one the chain will resolve for you: after a reconciliation window of your choosing, declare the hash not landed and re-sign at the same sequence. See Broadcasting and confirmation for the polling shape.
EVM-side failures
A compliance rejection on the EVM path becomes a revert, with the gate's error carried up verbatim as the revert reason. The same causes and the same messages reach you, in an Ethereum-shaped envelope.
The envelope is the part to get right. A revert is not a rejected submission:
eth_sendRawTransaction returns a hash, the transaction is mined, and the
receipt records the failure as status: 0x0. The reason is not in the receipt
— it is in the call's revert data, which an Ethereum receipt has no field for.
An eth_call with the same arguments returns it: a JSON-RPC error, code 3,
execution reverted: <the gate's message>.
| Cosmos path | EVM path | |
|---|---|---|
| A gate rejection appears | On the broadcast response, or on the transaction query | On the receipt — status: 0x0, without a reason |
| The transaction is | Usually not in a block | In a block |
| Gas | Not consumed | Consumed up to the revert |
| Diagnose it with | The gate trace, or simulate_trace before sending | An eth_call of the same method — as a pre-flight, or replayed afterwards |
A gate trace is still recorded for an EVM-originated transfer, but it is keyed by the Cosmos transaction hash of the wrapping transaction — not by the Ethereum transaction hash your tooling reports.
Vanilla transfer and transferFrom revert unconditionally, before anything
else runs:
HKCHAIN: vanilla transfer disabled — use transferWithTravelRule
HKCHAIN: vanilla transferFrom disabled — use transferFromWithTravelRule
If an ERC-20 library reports an unexplained revert on a transfer that looks correct, this is almost always it: the library called the standard method.
Where to go next
- Events and indexing — the durable record of what did happen.
- Integration patterns — where to put these checks in a real system.