Liquidium

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

Type

Prefix

Purpose

Inflow Deposit

0x1

User deposits

Inflow Repay

0x2

Debt repayments

Mapped Outflow

0x3

Direct address withdrawals

Native Outflow

0x5

IC Principal transfers

Boost

Internal

Small 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

Task

Interval

Purpose

check_for_new_subaccount_inflows

60s

Scan for new deposits/repayments

process_treasury_movements

60s

Detect confirmed deposits, create events

process_event_queue

60s

Send notifications to lending canister

process_boosted_withdrawals

300s

Batch small withdrawals

check_boosted_withdrawals_status

60s

Monitor on-chain confirmations

burn_accumulated_boost_ckbtc

60s

Recoup fronted BTC

frozen_utxos_cleanup

12h

Remove 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:

rust
// 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:

rust
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:

rust
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:

rust
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