Chain Client Implementation Guide

This guide explains how to implement a new ChainClient in THORChain's Bifrost module. It assumes the chain has passed the evaluation process and received Node Mimir approval.

Chain clients enable THORChain to:

  • Observe inbound L1 transactions
  • Sign and broadcast outbound vault transactions
  • Track vault balances and emit solvency reports
  • Handle chain-specific behaviors like mempool handling, reorgs, and gas estimation

Most chains extend existing clients — see EVM, UTXO, or BFT.

Directory Layout

All chain clients live under:

/bifrost/pkg/chainclients

If your chain fits an existing type (EVM, UTXO, Cosmos), you should place your client in the corresponding shared folder and extend the base implementation. This allows you to reuse common logic and minimize custom code.

FolderTypePurpose / NotesExample Chains
ethereum/CustomEthereum-specific EVM clientETH
evm/SharedGeneric EVM client for non-Ethereum chainsAVAX, BSC, BASE, POL
utxo/SharedFor Bitcoin-style UTXO chainsBTC, LTC, BCH, DOGE, ZEC
gaia/SharedFor Cosmos SDK / Tendermint chainsGAIA, NOBLE
solana/CustomCustom Solana clientSOL
tron/CustomCustom TRON clientTRON
xrp/CustomCustom Ripple clientXRP

How to Choose

  • Use an existing shared type folder (evm/, utxo/, gaia/) if your chain's architecture matches.
  • Use a new folder (e.g. solana/, xrp/, tron/) only if the chain requires custom observation, signing, or RPC handling that doesn't align with existing types.

Example:

  • utxo/bitcoin.go – Bitcoin-specific config using shared UTXO logic in utxo/client.go
  • evm/client.go – Generic EVM client used by AVAX, BSC, BASE, POL
  • ethereum/ethereum.go – Ethereum-specific client with custom gas and confirmation logic
  • gaia/cosmos_client.go – Cosmos client used by GAIA and NOBLE
  • xrp/client.go – XRP-specific logic using custom implementation

Required Interfaces

You must implement the following core interfaces:

InterfaceDefined InPurpose
ChainClientshared/types/types.goMain interface for observation, signing, solvency
BlockScannerFetcherbifrost/blockscanner/blockscanner.goScans blocks, mempool, and reports network fees
SolvencyCheckProvidershared/runners/solvency.goSolvency reporting (height, should-report, report)

See the ChainClient interface and BlockScannerFetcher interface for method-level detail.

Vault Address Derivation

Every client must derive vault addresses from the TSS public key:

ChainAlgoMethod
BitcoinECDSAP2WPKH from compressed pubkey
EthereumECDSAkeccak256(pubkey)[12:], checksummed
SolanaEDDSABase58-encoded ed25519

Use:

btcec.PublicKey.SerializeCompressed()     // ECDSA
edwards25519.PublicKey.Bytes()            // EDDSA

Implement:

func (c *YourChain) GetAddress(pubkey common.PubKey) string
  • Handle both PubKey and PubKeyEddsa
  • Ensure address format is deterministic

Observation Logic

Each client must observe inbound txs and forward them to THORChain.

Implement the BlockScannerFetcher interface:

func (c *YourScanner) FetchTxs(fetchHeight, chainHeight int64) (types.TxIn, error)
func (c *YourScanner) FetchMemPool(height int64) (types.TxIn, error)
func (c *YourScanner) GetHeight() (int64, error)
func (c *YourScanner) GetNetworkFee() (transactionSize, transactionFeeRate uint64)

Inbound txs must:

  • Be directed to active or retiring vaults
  • Include a valid THORChain memo
  • Be pushed to the global tx queue:
globalTxsQueue <- types.TxIn

Dust Threshold

Prevent spam by defining a dust threshold for your chain in common/chain.go via the DustThreshold() method. This determines the minimum inbound amount considered valid.

Memo Parsing

Memos must be:

  • Present
  • Decoded and parsed via x/thorchain/memo
  • Rejected if invalid or missing

Confirmation Counting

Chains with delayed finality (EVM, UTXO) must track confirmations:

func (c *YourChain) GetConfirmationCount(txIn types.TxIn) int64
  • For chains with instant finality (Cosmos, Solana, XRP, Avalanche), return 0.
  • For chains requiring dynamic confirmations (UTXO, Ethereum), compute based on transaction value relative to block reward using ConfMultiplier and cap with MAXCONFIRMATIONS-<CHAIN> (set via Mimir).
  • Some EVM chains use hard-coded confirmation counts (e.g., BSC=3, BASE=12, POL=15).

Outbound Signing

Outbound txs are signed by the TSS and passed to your ChainClient.

Implement:

SignTx(txOut types.TxOutItem, height int64) ([]byte, []byte, *types.TxInItem, error)
BroadcastTx(txOut types.TxOutItem, rawTx []byte) (string, error)
  • Encode signature correctly (ECDSA r,s,v or EDDSA)
  • Return the tx hash
  • Submit outbound observation via MsgOutboundTx

THORChain does not verify the tx on-chain — incorrect hashes are slashable.

Gas Estimation

Clients must report network fees via the BlockScannerFetcher.GetNetworkFee() method, which returns the transaction size and fee rate. This data is submitted to THORChain via the network fee queue and used to calculate outbound gas costs.

See Gas Tracking for details.

Reorg Handling

If a previously observed tx disappears due to a reorg:

  • Emit an ErrataTx
  • Allow THORChain to revert state

Ensure short-range reorgs are gracefully handled in block polling logic.

Solvency Reporting

Clients must emit vault balance data periodically:

globalSolvencyQueue <- types.Solvency

Missing funds (beyond PermittedSolvencyGap) will halt the chain.

Stuck Transactions

Clients must detect stuck outbound txs due to:

  • Low gas
  • Dust
  • Mempool eviction

EVM chains must implement the unstuck logic:

  • Reuse the same nonce
  • Send a 0-value tx to self
  • Use max(gasPrice × 1.1, 2 × current median gas)

Chain Registration

New chain clients must be registered in bifrost/pkg/chainclients/loadchains.go within the LoadChains() function. Add a case to the switch statement mapping your chain constant to its constructor:

case common.YOURChain:
    return yourchain.NewClient(thorKeys, chain, server, thorchainBridge, m, ...)

You must also register the chain in x/thorchain/manager_network_current.go in the supportChains array, and update 30+ locations in the common/ package (chain constant, gas asset, address derivation, signing algorithm, dust threshold, etc.). See the Chain Clients Overview for the full list of shared responsibilities.

Claude Code Skill

The /chain-client Claude Code skill can guide you through the full implementation process interactively. It includes decision trees, chain-family-specific patterns, registration checklists, and a database of 80+ historical bugs to avoid. Run /chain-client in Claude Code to get started.

Testing & Simulation

Before submitting your PR:

  • Launch the chain on stagenet or testnet

  • Test:

    • Inbound observation
    • Outbound signing
    • Vault churn
    • Memo parsing
    • Fee behavior
    • Errata + solvency
  • Use:

Also see New Chain Process for required test volumes.