Hkchain

The extended ERC-20 interface

Each EVM-visible HKD denomination is served by a precompile at its own fixed address. To an Ethereum client it looks like an ordinary ERC-20 token — the same read methods, the same Transfer and Approval events — with one deliberate departure: the standard transfer methods revert, and two extended methods carry the Travel Rule payload the chain requires.

ABI: precompiles/erc20/abi.json. Solidity interface: contracts/src/IHkchainHKD.sol.

There is no deployed contract behind these addresses. The implementation is Go code inside the node, which is why it can run the same compliance gates the Cosmos path runs.

Discovering the addresses

Do not hard-code them. Ask the chain:

curl -s "$REST/cosmos/evm/erc20/v1/token_pairs"

Each pair maps a denom to its ERC-20 address. Select the pair whose denom is the stablecoin you want; that pair's erc20_address is what you point a wallet or a contract call at.

Two properties of that mapping shape how you should design around it:

  • The addresses are deterministic per release, not per deployment. The set of compliance-enforcing HKD pairs is declared in the node software, so every network running the same release serves a given denom at the same address. Querying is still the right interface — it is what keeps a client correct across releases and across new issuers — but do not expect the values to differ between today's networks.
  • Onboarding an issuer is a release-and-genesis change. A new hkd.<issuer> enters the registry in the node software and reaches a running network through genesis or a coordinated upgrade. The set of EVM-visible stablecoins therefore changes at network-upgrade cadence, not by transaction.

The token-pair list also contains the wrapped native gas token. That pair is a standard wrapped-native primitive, not a stablecoin, and it does not carry the compliance gates — filter on the hkd. denom prefix rather than taking every pair.

Methods

MethodBehaviour
name(), symbol(), decimals(), totalSupply()Standard reads, inherited unchanged
balanceOf(address)Standard. The balance is the account's bank balance in that denom — the same number the Cosmos balance query returns
allowance(address,address), approve(address,uint256)Standard allowance handling, inherited unchanged
transfer(address,uint256)Reverts, always
transferFrom(address,address,uint256)Reverts, always
transferWithTravelRule(address to, uint256 amount, bytes travelRulePayload) → boolThe supported transfer
transferFromWithTravelRule(address from, address to, uint256 amount, bytes travelRulePayload) → boolThe supported allowance-mediated transfer

The two vanilla transfers revert before anything else runs, with a readable reason:

HKCHAIN: vanilla transfer disabled — use transferWithTravelRule
HKCHAIN: vanilla transferFrom disabled — use transferFromWithTravelRule

They revert unconditionally — not "when a payload is missing". A plain ERC-20 transfer carries no Travel Rule data, so honouring it would open a compliance-free path to the same balances. If a generic ERC-20 library reports an unexplained revert on a transfer that looks correct, this is almost always the cause: the library called the standard method.

approve is not gated. Granting an allowance moves no value; the gates run when the allowance is spent through transferFromWithTravelRule.

The payload argument

travelRulePayload is a protobuf-serialised hkchain.compliance.v1.TravelRulePayload, passed as bytes. Not JSON, not an ABI-encoded struct — the same message the Cosmos path attaches as a transaction extension option, in its binary encoding.

Its fields, and which of them you populate, are decided by the sender's registered Travel Rule tier rather than by the amount you happen to be sending. Attach a Travel Rule payload and the TravelRulePayload schema in hkchain.compliance.v1 are the reference; the EVM path imposes no additional fields and relaxes none.

Malformed bytes revert with HKCHAIN: malformed TravelRulePayload bytes: … before the gates run at all.

The tx_hash field of the payload is left empty by the caller — the chain derives the binding itself.

What happens on a transfer

An extended transfer is not a thin wrapper over a bank send. In order:

  1. The payload bytes are decoded.
  2. A bank send message is synthesised from the caller, the recipient, and the amount in the precompile's denom, carrying the decoded payload.
  3. The same compliance gate chain the Cosmos path uses runs over that synthesised transaction.
  4. For the transferFrom variant, the allowance is checked and decremented.
  5. The bank send executes, and Transfer — plus Approval for the transferFrom variant — is emitted.

A gate rejection stops it at step 3, and the gate's message becomes the revert reason.

This runs during EVM execution, not during transaction admission. That is the difference an integrator has to design around: on the Cosmos path a gate rejection means the transaction is usually never included in a block, while here the transaction is mined and fails. The receipt records status: 0x0, gas is consumed up to the revert, and the reason is in the call's revert data rather than in the receipt. Recover it with an eth_call of the same method and arguments, which returns a JSON-RPC error with code 3 and execution reverted: <the gate's message>. Errors and rejections has the full comparison.

Gas: the extended methods charge the standard transfer cost plus a fixed compliance stipend of 30,000, so budget above what a plain ERC-20 transfer would need.

Events and indexing

An extended transfer emits the ERC-20 Transfer event that any ERC-20 consumer expects, so balance tracking works normally.

It does not emit an Hkchain typed transfer event — no such event exists on either path. What the same transfer additionally leaves is Cosmos-side: the bank module's own events and an entry in the compliance audit log.

One trap when searching Cosmos events for EVM-originated transfers: the sender appears twice, in two encodings. The EVM module records the hex sender as message.sender, and the bank send underneath records the bech32 sender as message.sender as well. A bech32 search therefore finds stablecoin transfers from either path; the hex form narrows to EVM-originated ones. There is no ethereum_tx.sender attribute, and ethereum_tx.recipient is the contract called — the token's precompile address — not the payee.

The gate trace for an EVM-originated transfer is keyed by the Cosmos transaction hash of the wrapping transaction, not by the Ethereum hash your tooling reports.

Reading state from the EVM side

Every read method answers from the same state the Cosmos queries read; there is no separate EVM ledger. Choose whichever is convenient.

What is not available over JSON-RPC is everything Hkchain-specific: identity records, the gate trace, flags, reserve attestations and the audit log are Cosmos queries only. An EVM-first integration still needs a REST client for those — in particular for the pre-flight identity check that avoids a reverted, gas-consuming transfer.

Parity, and its boundary

Any rule enforced on a Cosmos MsgSend of hkd.* is enforced on transferWithTravelRule of the same denom. That is the property the vanilla methods are disabled to protect: a compliance-free ERC-20 path to the same balances would defeat the chain-level enforcement model entirely.

The parity claim is about those two transfer shapes. It is not a claim that every message the chain accepts is screened — Transaction lifecycle documents which message shapes the gates actually project a compliance fact from.