Introduction

Bifrost is THORChain’s cross-chain bridge module that handles the observation and validation of external blockchain transactions. It ensures that inbound and outbound transactions are correctly seen, confirmed, and relayed across chains, while protecting the protocol from issues like re-orgs, double-spends, and vault insolvency.

Bifrost Components

Each chain client in Bifrost is split into two roles:

  • Observer: Scans external blocks and detects transactions involving THORChain vaults. It prepares observations for inbound transactions and forwards them to THORChain for consensus.
  • Signer: Constructs and signs outbound transactions based on TxOut data received from THORChain. It uses TSS to securely sign and broadcast these transactions.

Both components are implemented per-chain and tailored to the nuances of each blockchain.

ObservedTx Structure

Every inbound or outbound transaction that passes through Bifrost is wrapped in an ObservedTx struct, which captures not just the transaction data, but metadata required for consensus, validation, and execution.

ObservedTx {
  Tx                    Tx     // The raw transaction: chain, from/to, coins, memo, gas
  Status                Status // Processing status of the transaction
  OutHashes             []string // Outbound tx hashes generated by this inbound
  BlockHeight           int64  // THORChain block height this observation was registered
  Signers               []string // List of node accounts that observed the tx
  ObservedPubKey        PubKey // Vault pubkey that received or sent the tx
  KeysignMs             int64  // Duration of TSS signing in milliseconds (outbounds only)
  FinaliseHeight        int64  // Block height when transaction reached consensus
  Aggregator            string // Used for contract calls like transferOutAndCall
  AggregatorTarget      string // Target contract (if aggregator used)
  AggregatorTargetLimit *Uint  // Limit passed to aggregator contract
}

The ObservedTx struct is used across the following THORChain message types:

  • MsgObservedTxIn – submitted by validators when inbound transactions are observed on external chains.
  • MsgObservedTxOut – submitted after a node broadcasts a signed outbound transaction.
  • MsgTssKeysignFail – reports a failure during the TSS ceremony for an outbound.
  • MsgTssKeysignRetry – requests retry of a previously failed outbound.
  • MsgTssKeygenMetric – logs keygen ceremony statistics for monitoring validator behavior.

Purpose and Usage

ObservedTx structures serve several critical roles within Bifrost and THORChain's consensus flow:

  • Inbound Transactions: When a transaction is detected on an external chain and gossiped by nodes, it becomes part of a MsgObservedTxIn, which wraps one or more ObservedTx instances. These are submitted once a supermajority of validators (≥67%) report the same transaction.
  • Outbound Transactions: After a TSS signing ceremony and broadcast, nodes submit a MsgObservedTxOut, including an ObservedTx reflecting the actual broadcast result. This confirms the transaction reached the external chain.
  • Consensus Tracking: Each ObservedTx tracks which nodes signed or attested to an event, its processing status, and when final consensus was reached (FinaliseHeight). This allows THORChain to detect divergence and apply slashing if needed.
  • Slashing and Retries: Repeated or late-signing nodes are tracked using ObservedTx entries. By caching status and signers, the protocol ensures idempotency and enforces accountability.
  • Aggregator Metadata: Used in DeFi and contract interactions (e.g., transferOutAndCall), ObservedTx can carry details about targets and limits.

Transaction Flow Overview

Inbound Transactions

  1. Each THORNode runs a full node for every supported external chain. Each Bifrost chain client (e.g., Bitcoin, Ethereum) connects to its local full node to scan for relevant transactions. It queries the chain via RPC or direct database access depending on the chain client.
  2. Each node observes transactions sent to Active or Retiring vaults by scanning each new block from the connected chain. Transactions to Inactive vaults are only refunded if the vault still has 67% of its nodes online (including Standby and Ready).
  3. If a transaction is relevant, it's broadcast (gossiped) to the Bifrost network.
  4. Once 67% of nodes have observed the same transaction, it’s submitted to THORChain as a MsgObservedTxIn.
  5. Once finalize, the transaction enters THORChain’s queue for processing.

Outbound Transactions

  1. THORChain decides on an outbound transaction and assigns it to a vault.
  2. The vault uses TSS signing to construct and broadcast the transaction.
  3. The signing nodes immediately submit a MsgObservedTxOut confirming the outbound was broadcast. Other nodes do not independently verify the transaction on-chain.
  4. Once 67% consensus is reached, the transaction is marked as completed.
  5. Signed transactions are cached locally to prevent duplicate signing or rebroadcasts after retries or restarts. This ensures idempotency and protects against slashing for duplicate outbounds.

Observations and Consensus

  • Supermajority Required: A minimum of 67% of active nodes must attest to a transaction for it to be finalized.
  • Vault Status Rules:
    • Active and Retiring vaults: fully process transactions.
    • Inactive vaults: Refunds are only attempted if at least 67% of nodes in the vault are still online, meaning they are in Active, Standby, or Ready status.
    • If more than 33% of the vault's participants have left the network (no longer running nodes), then the vault is considered unrecoverable and no refund can be made.
  • Slashing: Nodes are slashed for:
    • Not observing valid transactions
    • Broadcasting incorrect observations
  • Mempool Handling: For most chains, mempool transactions are ignored unless both the sender and recipient are THORChain vaults. This includes internal vault-to-vault actions such as consolidations or migrations.

Finality & Pre-Confirmation

Instant-Finality Chains (e.g. Cosmos SDK, BFT)

  • Inbound transactions are immediately final once included in a block.
  • No pre-confirmation logic is needed.

Delayed-Finality Chains (e.g. UTXO, EVM)

  • Transactions enter a pre-confirmation state when first observed.
  • These are exposed in the THORChain API with CommittedUnFinalised = true.
  • A second round of observation occurs once sufficient confirmations are seen.
  • The transaction is then marked final and eligible for processing.

Finalization occurs when FinaliseHeight == BlockHeight, meaning the transaction has reached sufficient confirmations and is now safe to process. Finality occurs when FinaliseHeight equals BlockHeight.

Confirmation Requirements

The number of confirmations is calculated dynamically based on:

  • Total transaction value
  • Block reward (or fee + subsidy for UTXO)
  • A chain-specific confirmation multiplier (Mimir)
  • A maximum cap per chain (Mimir)

This is implemented as:

RequiredConfirmations = min(
    (TxValue / BlockReward) × ConfMultiplier,
    MAXCONFIRMATIONS
)

Where:

  • ConfMultiplier is pulled via GetMimirWithRef using a chain-scoped key
  • MAXCONFIRMATIONS is also Mimir-configured per chain
  • UTXO chains use getCoinbaseValue(height) to retrieve the reward+fees
  • EVM chains use getBlockReward(height) (Post-Merge PoS logic is handled here)
  • A minimum (e.g., ETH requires at least 2 confirmations) is enforced in code

Example Mimir Keys and Defaults

ChainMax Confirmations Mimir KeyDefault Max Confirmations
BTCMAXCONFIRMATIONS-BTC2
ETHMAXCONFIRMATIONS-ETH14
LTCMAXCONFIRMATIONS-LTC6
BCHMAXCONFIRMATIONS-BCH3
DOGEMAXCONFIRMATIONS-DOGE15

The actual confirmation logic is implemented in the getBlockRequiredConfirmation() function within the relevant ChainClient, and varies slightly between UTXO and EVM chains. Both rely on the same utility helpers to pull values from Mimir (GetConfMulBasisPoint and MaxConfAdjustment).

Note: The CONFIRMATIONMULTIPLIER-* Mimir keys are currently not active (not set in Mimir). In practice, a default multiplier of 10,000 basis points (i.e. 1x) is used unless overridden.

Economic Security Rationale

  • Higher-value transactions require more confirmations.
  • This reduces the chance of loss from chain reorganizations.
  • The capped value ensures UX is not degraded on high-latency chains

Re-orgs & Errata

Re-orgs occur when an external chain replaces previously confirmed blocks, causing transactions to disappear from its history. This can result in THORChain processing a transaction that no longer exists on the external chain, leading to vault imbalance or insolvency.

THORChain uses errata transactions to detect and reverse these inconsistencies.

Detection

  • Nodes monitor external chains for previously observed transactions that have disappeared.
  • If a re-org is detected, the node creates a MsgErrataTx and gossips it to peers.
  • Each node maintains a local transaction history cache (~24 hours) to assist with this detection.

Consensus

  • Once 67% of active nodes agree on the errata, it is applied.

Corrections

  • Inbound — Transaction is removed from the queue; vault balances are reverted.
  • Outbound — Funds are credited back to the vault; the outbound is rescheduled.
  • Pools — Asset balances are adjusted to correct any inflation or deflation.
  • Vaults — If the affected vault was inactive, it may be temporarily marked Retiring to enable migration.
  • Security Events — Emitted to alert observers of the inconsistency and recovery action.

Stuck or Dropped Transactions

Outbound transactions may occasionally get stuck in the mempool or dropped due to network congestion, low gas, or other external factors. THORChain implements chain-specific mechanisms to detect and recover from these conditions.

For example:

  • UTXO chains can recover stuck transactions via CPFP
  • EVM chains require same-nonce replacement transactions

See the relevant Chain Client implementation for more details.

Double-Spend Mitigation

Scenario

  1. Vault A signs and broadcasts an outbound transaction (e.g. with low gas or during network congestion).
  2. The transaction is not confirmed on the external network and remains pending.
  3. After a timeout, THORChain triggers a reschedule and reassigns the outbound to Vault B.
  4. Vault B successfully signs and broadcasts a new transaction, which gets confirmed.
  5. Later, Vault A's original transaction finally confirms on the external chain.
  6. This results in a double-spend — funds were sent twice.

If a double-spend or failed outbound is not properly suppressed, it can lead to an imbalance between THORChain’s internal vault accounting and on-chain balances, triggering a solvency alert.

Prevention Mechanism

THORChain uses the LackSigning mechanism to detect and prevent double-spends:

  • LackSigning runs every block.
  • It checks for outbound transactions that:
    • Have not been observed (OutHash is empty),
    • Are older than SigningTransactionPeriod blocks (default: 300 blocks),
    • Are not internal migrations,
    • Are not assigned to an inactive or frozen vault,
    • Pass the needsNewVault check.

If these conditions are met, the transaction is rescheduled to a new vault. The original vault's transaction is tracked. If it later appears on-chain after the rescheduled transaction has been confirmed, it is ignored. This suppresses slashing and ensures only the final successful outbound is honored. Duplicate outbounds from the same vault are safely discarded if they arrive late.

This mechanism protects THORChain from accidental or malicious double-spends caused by network delays, stuck mempool transactions, or external chain instability.

Gas Tracking

Accurate gas pricing is critical to ensure outbound transactions are mined promptly and do not get stuck. THORChain nodes observe and report gas rates on external chains, and Bifrost uses this data to calculate the correct gas to use when signing outbound transactions.

Gas Price Observation

Each THORNode observes external gas prices using chain-specific logic. For UTXO chains (like BTC), if getblockstats is not available, the node parses blocks directly:

  • The gas price (fee rate) is calculated as:

    feeRate = (TotalFeesInBlock) / (TotalVSize)
    
  • Fee rate is then:

    • Rounded to the nearest multiple of GasPriceResolution
    • Compared with DefaultMinRelayFeeSats, and raised if below
    • Only reported if changed significantly from the last report

This reduces fee volatility and prevents spam updates.

Gas Price Reporting

  • Nodes report gas prices:

    • Every 100 blocks (fallback).
    • Or when the gas price changes significantly (beyond GasPriceResolution).
  • Gas prices are stored over a cache window (GasCacheBlocks), typically 40–100 blocks.

  • The gas rate ratchets up quickly and down slowly to prevent manipulation or griefing.

Gas amounts and gas assets are also included in ObservedTx for each outbound. These values are used to charge liquidity pools and maintain protocol solvency.

Calculation

Transaction Execution Gas

When Bifrost signs an outbound transaction, it uses:

gasRate = observedGasRate × 1.5

This ensures the transaction is mined quickly, even in volatile or congested networks. This logic is applied in GetGasDetails(), except for native THORChain transactions which use known gas values.

  • The median gas price from the cache is used.
  • It is rounded up to the nearest multiple of GasPriceResolution.
  • The outbound transaction uses 1.5× the reported gas price to ensure it is mined in the next block.

This approach prevents underpriced transactions and ensures timely execution, especially on congested networks.

Outbound Fee Charged to Users

The actual fee charged to users (for swaps, withdrawals, etc.) is:

Fee = (1.5 × observedGasRate) × OFM

Where OFM (Outbound Fee Multiplier) is a dynamic value that adjusts based on:

  • Protocol reserve balance
  • Gas asset pool balances
  • Surplus or deficit in prior fee reimbursements

The multiplier increases when the protocol is subsidizing gas too heavily and decreases when it's overcharging. See How the OFM works for more details.