Liquidium LogoLiquidium
TechnicalArchitecture

BTC Pool Canister

Bitcoin liquidity custody with boosted withdrawals and UTXO management

Responsibilities

  • Accept ckBTC deposits from users
  • Process withdrawals (converting ckBTC → BTC)
  • Handle borrow requests from the lending canister
  • Optimize fees via withdrawal batching for small amounts
  • Manage UTXOs for the pool's Bitcoin address

Architecture

Subaccount Architecture

The pool uses deterministic subaccount derivation for user isolation:

Subaccount Types

TypePrefixPurpose
Inflow Deposit0x1User deposits
Inflow Repay0x2Debt repayments
Mapped Outflow0x3Direct address withdrawals
Native Outflow0x5IC Principal transfers
BoostInternalSmall withdrawal batching

Inflow Subaccounts

For deposits and repayments, subaccounts are derived from the user's principal:

[prefix, 0x0, length, ...principal_bytes..., ...padding]
 byte 0  byte 1 byte 2     bytes 3-N          bytes N-31

Example - Deposit subaccount:

[0x1, 0x0, 0x0A, <10 principal bytes>, <19 zero bytes>]

Outflow Subaccounts (Mapped)

Bitcoin addresses can be long, so they're mapped to a u128 index:

[0x3, <15 zero bytes>, <16 bytes of u128 index>]

The pool maintains bidirectional mappings:

  • ADDRESS_OUTFLOW_SUBACCOUNT: address → index
  • ADDRESS_OUTFLOW_SUBACCOUNT_REVERSE: index → address

Special Subaccounts

BOOST_SUBACCOUNT:

[0x0, 0x1, <30 zero bytes>]

Holds ckBTC for boosted withdrawals awaiting batching.

Two-Tier Withdrawal System

Standard Withdrawals (>50,000 sats)

Direct ckBTC burn via the minter:

Boosted Withdrawals (Under 50,000 sats)

Problem: Bitcoin transaction fees make small withdrawals uneconomical.

Solution: Batch multiple small withdrawals into a single Bitcoin transaction.

Boosted Withdrawal Flow:

  1. Pool transfers ckBTC to BOOST_SUBACCOUNT
  2. Withdrawal added to pending queue
  3. Every 5 minutes, pending withdrawals are processed (even if there's only one)
  4. Pool creates multi-output Bitcoin transaction (or single output if only one withdrawal)
  5. Transaction signed using threshold ECDSA
  6. Transaction broadcast to Bitcoin network
  7. Pool fronts BTC immediately from its UTXOs
  8. When boost balance exceeds 50k sats, accumulated ckBTC is burned

Benefits:

  • Users receive BTC faster
  • Lower effective fees (shared across batch)
  • Pool recoups fronted BTC via ckBTC burn

Inflow Detection

The pool scans for new deposits and repayments every 60 seconds:

Treasury Movement Detection

After inflows are transferred to treasury, the pool detects and creates events:

Background Tasks

TaskIntervalPurpose
check_for_new_subaccount_inflows60sScan for new deposits/repayments
process_treasury_movements60sDetect confirmed deposits, create events
process_event_queue60sSend notifications to lending canister
process_boosted_withdrawals300sBatch small withdrawals
check_boosted_withdrawals_status60sMonitor on-chain confirmations
burn_accumulated_boost_ckbtc60sRecoup fronted BTC
frozen_utxos_cleanup12hRemove stale UTXO locks

UTXO Management

The pool manages UTXOs for its Bitcoin address:

UTXO Freezing

When UTXOs are used in a transaction, they're temporarily frozen to prevent double-spending:

// After broadcasting transaction
for input in used_inputs {
    FROZEN_UTXOS.insert(input.to_string(), timestamp);
}

UTXO Selection

For boosted withdrawals, UTXOs are selected largest-first:

let mut utxos = get_available_utxos();
utxos.sort_by(|a, b| b.value.cmp(&a.value));  // largest first

let (selected, fee, total) = fund_transaction(&mut tx, utxos, fee_rate);

Cleanup

Frozen UTXOs are released after 12 hours if their transaction hasn't confirmed:

fn frozen_utxos_cleanup() {
    let cutoff = now() - 12 * 3600;
    for (utxo, freeze_time) in FROZEN_UTXOS {
        if freeze_time < cutoff {
            FROZEN_UTXOS.remove(&utxo);
        }
    }
}

Threshold ECDSA Signing

Bitcoin transactions are signed using ICP's threshold ECDSA:

let signature = sign_with_ecdsa(SignWithEcdsaArgument {
    message_hash: sighash_data,
    derivation_path: vec![],
    key_id: EcdsaKeyId {
        curve: EcdsaCurve::Secp256k1,
        name: KEY_NAME.to_string(),
    },
}).await;

This provides:

  • Decentralized key management
  • No single point of failure
  • Cryptographic security guarantees