Technical Details

As discussed in the previous chapter, BitoBridge’s operations comprise multiple canisters. The DFINITY team developed the ckBTC Ledger, ckBTC Ledger, ckBTC Minter, and Bitcoin canisters. The Bitomni team crafted and deployed essential components, including the BitoBridge Hub, BitoBridge Minter, BitoBridge Bitcoin canisters, and the smart contracts of Ledger and Helper on multiple blockchains.

Please refer to Chain-key Bitcoin's design document for technical details of the ckBTC Ledger, ckBTC Ledger, ckBTC Minter, and Bitcoin canisters. The ones developed by the Bitomni team are discussed below.

BitoBridge Hub Canister

The BitoBridge Hub collaborates with the BitoBridge Minter to initiate minting tasks and with the ckBTC/ICRC Ledger to facilitate token transfers. When tokens are transferred, they are effectively locked in a designated subaccount for the target blockchain under the Hub canister’s account. For instance, in supporting the Ethereum blockchain, BitoBridge utilizes a specific subaccount linked to Ethereum, established via the principal_to_subaccount(), as demonstrated in the following code block:

pub fn principal_to_subaccount(principal_id: &Principal) -> Subaccount {
    let mut subaccount = [0; std::mem::size_of::<Subaccount>()];
    let principal_id = principal_id.as_slice();
    subaccount[0] = principal_id.len().try_into().unwrap();
    subaccount[1..1 + principal_id.len()].copy_from_slice(principal_id);

    subaccount
}

It stores data about the supported chains and tokens and metadata for various blockchains, including the ID of each blockchain's Minter canister, configurations, and more.

BitoBridge Minter Canister

The Minter canister actively engages with the Ledger on the target blockchain to mint wrapped versions of ICRC tokens. For instance, BITO.eth represents the Ethereum-wrapped version of the BITO token on the Ethereum blockchain, as demonstrated in the following code block:

async fn send_transactions_batch(latest_transaction_count: Option<TransactionCount>) {
    let latest_transaction_count = match latest_transaction_count {
        Some(latest_transaction_count) => latest_transaction_count,
        None => {
            return;
        }
    };
    let transactions_to_send: Vec<_> = read_state(|s| {
        s.eth_transactions
            .transactions_to_send_batch(latest_transaction_count, TRANSACTIONS_TO_SEND_BATCH_SIZE)
    });

    let rpc_client = read_state(EthRpcClient::from_state);
    let results = join_all(
        transactions_to_send
            .iter()
            .map(|tx| rpc_client.eth_send_raw_transaction(tx.raw_transaction_hex())),
    )
    .await;

    for (signed_tx, result) in zip(transactions_to_send, results) {
        log!(DEBUG, "Sent transaction {signed_tx:?}: {result:?}");
        match result {
            Ok(JsonRpcResult::Result(tx_result)) if tx_result == SendRawTransactionResult::Ok || tx_result == SendRawTransactionResult::NonceTooLow => {
                // In case of resubmission we may hit the case of SendRawTransactionResult::NonceTooLow
                // if the stuck transaction was mined in the meantime.
                // It will be cleaned-up once the transaction is finalized.
            }
            Ok(JsonRpcResult::Result(tx_result)) => log!(INFO,
                "Failed to send transaction {signed_tx:?}: {tx_result:?}. Will retry later.",
            ),
            Ok(JsonRpcResult::Error { code, message }) => log!(INFO,
                "Failed to send transaction {signed_tx:?}: {message} (error code = {code}). Will retry later.",
            ),
            Err(e) => {
                log!(INFO, "Failed to send transaction {signed_tx:?}: {e:?}. Will retry later.")
            }
        };
    }
}

This canister also monitors events from the blockchain’s RPC nodes, processing the unwrapping of tokens and their conversion back to their original ICRC forms on the Internet Computer network. Each supported blockchain has its dedicated Minter canister, ensuring tailored operations and interactions for each blockchain environment.

BitoBridge Bitcoin Canister

The Bitcoin canister calls the ckBTC MInter canister's get_btc_address() function which makes an asynchronous request to fetch the Bitcoin address. It passes a struct which contains two parameters to this service:

  • owner: This parameter uses ic_cdk::id()the identifier of the current canister, which links the request explicitly to this canister.

  • subaccount: This parameter is derived from the user's principal identifier and transformed into a subaccount format. This differentiation allows for addressing specific user accounts under the same principal.

pub async fn get_btc_address(&self) -> Result<String, BtcIcrcError> {
        let res = self.ckbtc_service.get_btc_address(GetBtcAddressArg{
            owner: Some(ic_cdk::id()),
            subaccount: Some(ByteBuf::from( principal_to_subaccount(&self.user_principal))),
        }).await;
        match res {
            Ok(address) => Ok(address.0),
            Err(e) => {
                let msg = format!("failed to call ckbtc_service.get_btc_address: {:?}", e);
                Err(BtcIcrcError::TemporarilyUnavailable {
                    message: msg,
                })
            }
        }
    }

Bitcoin Integration from IC All Bitcoin addresses under the ckBTC minters control are P2WPKH (“pay to witness public key hash”) addresses as defined in BIP-141. These addresses are rendered in the Bech32 format as defined in BIP-173. The main advantage of P2WPKH over the legacy P2PKH address type is that its use results in lower transaction fees.

While the ckBTC minter exclusively uses P2WPKH addresses internally, it supports all currently used address formats (P2PKH, P2SH, P2WPKH, P2TR) for retrievals.

The user deposits Bitcoin using the provided btc_address. Typically, after waiting for 6 confirmations, the user would need to manually trigger the update_balance() function to refresh UTXO and ckBTC balance information. To streamline this process, BitoBridge automates the balance updates by requiring users to link their Bitcoin and Internet Computer wallets. This integration allows the Bitcoin canisters to acquire the transaction hash automatically (tx_hash) and the user’s principal. A timer is set to periodically execute update_balance(), ensuring the user’s balances are consistently up-to-date without manual intervention.

BitoBridge Indexer Canister

It is designed to index and manage BitoBridge's data efficiently. This canister is a dynamic repository catalogs transaction histories, token metadata, and cross-chain interactions related to BitoBridge's operations. Its primary role is to provide quick access to essential data, enhancing the performance and user experience by facilitating rapid queries and retrieval of information. By maintaining a structured record of all blockchain activities facilitated by BitoBridge, the Indexer Canister ensures that users and systems can efficiently track and verify the status of any transaction or bridge event, contributing significantly to the system's transparency and trustworthiness.

The technical details for this canister are still under development, so it is not currently included in the architecture diagram.

BitoBridge Statistics Canister

It is an essential component designed to aggregate and provide analytical insights about the activities within the BitoBridge ecosystem. This canister processes and displays statistical data related to transaction volumes, frequency, user engagement, and other relevant metrics across different blockchain interactions facilitated by BitoBridge. Its functionality supports real-time monitoring and reporting, which helps optimize bridge performance, forecast network needs, and enhance user experiences. By offering a comprehensive view of the network's operations, the Statistics Canister enables stakeholders to make informed decisions and continuously improve the effectiveness of the BitoBridge services.

The technical details for this canister are still under development, so it is not currently included in the architecture diagram.

BitoBridge Ledger Contract

The Ledger operates as a smart contract, extending the functionality of the token contract for each blockchain it interacts with. An example of the Ledger’s implementation on EVM-compatible chains is illustrated in the following code snippet:

function mintTo(address to_, uint256 amount_) public {
        require(active, "Contract is not active");
        require(msg.sender == _minterAddress, "Only minter can mint");
        _mint(to_, amount_);
    }

To incorporate a new ICRC token into BitoBridge, a distinct token contract must be deployed on each supported blockchain. For instance, if we want to add the CHAT token to BitoBridge and the supported blockchains include Ethereum, Arbitrum, and Solana, then individual token contracts capable of minting CHAT.x must be deployed on each of these blockchains. These contracts are designed to allow unlimited token issuance. However, the mintTo() function responsible for token creation can only be executed by the address corresponding to the BitoBridge Minter canister. Importantly, this address is immutable and cannot be altered once set, ensuring a secure and controlled token distribution process.

BitoBridge Helper Contract

The Helper contract, deployed across each supported blockchain, manages the burning of wrapped tokens using the bridge() function demonstrated in the following code block. Burning these tokens emits an event captured by the corresponding BitoBridge Minter canister via RPC nodes. This event is then relayed to the BitoBridge Hub canister, which delegates the minting task to the target chain’s BitoBridge Minter canister. This final canister orchestrates the transaction through the target chain's RPC nodes to mint the wrapped token using the BitoBridge Ledger contract.

event Bridge(uint8 chain, address indexed erc20_address, address indexed from, uint256 amount, bytes32 indexed recipient);

function bridge(uint8 chain, address erc20_address_, uint256 amount_, bytes32 recipient_) public {
        IBito(erc20_address_).burnFrom(msg.sender, amount_);
        emit Bridge(chain, erc20_address_, msg.sender, amount_, recipient_);
    }

The process deviates from the standard sequence when the target chain is either the Bitcoin blockchain or IC. After the event reaches the BitoBridge Hub canister for transactions targeting IC, the token transfer is directly executed by invoking the ICRC Ledger canister, bypassing the BitoBridge Minter canister. Conversely, once the event reaches the BitoBridge Hub canister for Bitcoin transactions, it calls the ckBTC Ledger canister for approval. Following approval, the ckBTC Minter’s retrieve_btc_with_approval() function is triggered to authorize the burning of the specified ckBTC amount and dispatch the corresponding BTC amount, less applicable fees, to the designated Bitcoin address.

Last updated