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
| Method | Behaviour |
|---|---|
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) → bool | The supported transfer |
transferFromWithTravelRule(address from, address to, uint256 amount, bytes travelRulePayload) → bool | The 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:
- The payload bytes are decoded.
- A bank send message is synthesised from the caller, the recipient, and the amount in the precompile's denom, carrying the decoded payload.
- The same compliance gate chain the Cosmos path uses runs over that synthesised transaction.
- For the
transferFromvariant, the allowance is checked and decremented. - The bank send executes, and
Transfer— plusApprovalfor thetransferFromvariant — 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.