For Solidity Developers

The tools we use to work on Sova are the same as any other EVM network. Developers can use Foundry, Tenderly, common web3 frontend libraries, and much more. Sova can easily integrate with any other EVM network while at the same time benefiting from the hard work of the largest Blockchain dev community in the world.

Deposit Contract

// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

interface ISovaBTC {
    function depositBTC(uint64 amount, bytes calldata signedTx, uint8 voutIndex) external;
    function withdraw(uint64 amount, uint64 btcGasLimit, uint64 btcBlockHeight, string calldata dest) external;
    function isTransactionUsed(bytes32 txid) external view returns (bool);
}

Precompile Library

SovaBitcoin.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

/**
 * @title SovaBitcoin
 * @author Sova Labs
 *
 * A library for integrating with Bitcoin precompiles on Sova.
 */
library SovaBitcoin {
    /// @notice Bitcoin precompile address
    address public constant BROADCAST_TRANSACTION_PRECOMPILE_ADDRESS = address(0x999);
    address public constant DECODE_TRANSACTION_PRECOMPILE_ADDRESS = address(0x998);
    address public constant CONVERT_ADDRESS_PRECOMPILE_ADDRESS = address(0x997);
    address public constant VAULT_SPEND_PRECOMPILE_ADDRESS = address(0x996);

    /// @notice Bitcoin context contract address
    address public constant SOVA_L1_BLOCK_ADDRESS = 0x2100000000000000000000000000000000000015;
    /// @notice Native Bitcoin wrapper address
    address public constant UBTC_ADDRESS = 0x2100000000000000000000000000000000000020;

    struct Output {
        string addr;
        uint256 value;
        bytes script;
    }

    struct Input {
        bytes32 prevTxHash;
        uint32 outputIndex;
        bytes scriptSig;
        bytes[] witness;
    }

    struct BitcoinTx {
        bytes32 txid;
        Output[] outputs;
        Input[] inputs;
        uint256 locktime;
    }

    error PrecompileCallFailed();
    error InvalidOutput(string expected, string actual);
    error InvalidDeposit();
    error InvalidAmount();
    error InsufficientInput();
    error InvalidLocktime();

    /// @custom:semver 0.1.0-beta.1
    function version() public pure returns (string memory) {
        return "0.1.0-beta.1";
    }

    /**
     * @notice Decodes a raw Bitcoin transaction into a structured format
     *
     * @param signedTx          The raw signed Bitcoin transaction
     *
     * @return BitcoinTx        Object containing the decoded transaction data
     */
    function decodeBitcoinTx(bytes memory signedTx) internal view returns (BitcoinTx memory) {
        (bool success, bytes memory returndata) =
            DECODE_TRANSACTION_PRECOMPILE_ADDRESS.staticcall(abi.encodePacked(signedTx));
        if (!success) revert PrecompileCallFailed();
        return abi.decode(returndata, (BitcoinTx));
    }

    /**
     * @notice Retrieves the unique deposit address for the corresponding EVM address
     *
     * @param addr             The EVM address to convert
     *
     * @return returnData      The Bitcoin deposit address in bytes format
     */
    function convertToBtcAddress(address addr) internal returns (bytes memory) {
        (bool success, bytes memory returnData) = CONVERT_ADDRESS_PRECOMPILE_ADDRESS.call(abi.encodePacked(addr));
        if (!success) revert PrecompileCallFailed();
        return returnData;
    }

    /**
     * @notice Broadcasts a signed Bitcoin transaction to the Bitcoin network
     *
     * @param signedTx         The raw signed Bitcoin transaction to broadcast
     */
    function broadcastBitcoinTx(bytes memory signedTx) internal {
        (bool success,) = BROADCAST_TRANSACTION_PRECOMPILE_ADDRESS.call(abi.encodePacked(signedTx));
        if (!success) revert PrecompileCallFailed();
    }

    /**
     * @notice Validates a Bitcoin transaction for deposit purposes and returns the decoded transaction
     * @dev Performs a series of checks on the transaction structure and content:
     *      1. Verifies the transaction has between 1 and 3 outputs
     *      2. Confirms the first output value meets the minimum amount
     *      3. Ensures the transaction has at least one input
     *      4. Validates the locktime is not in the future
     *      5. Checks that the specific output address matches the network's receive address
     *
     * @param signedTx          The raw signed Bitcoin transaction
     * @param amount            The minimum expected amount in satoshis
     * @param voutIndex         The output index of the BTC tx that contains the deposit UTXO
     *
     * @return btcTx            The decoded Bitcoin transaction
     */
    function isValidDeposit(bytes memory signedTx, uint256 amount, uint8 voutIndex)
        internal
        returns (BitcoinTx memory)
    {
        BitcoinTx memory btcTx = decodeBitcoinTx(signedTx);

        if (btcTx.outputs.length < 1 || btcTx.outputs.length > 3) {
            revert InvalidDeposit();
        }

        if (btcTx.outputs[voutIndex].value != amount) {
            revert InvalidAmount();
        }

        if (btcTx.inputs.length < 1) {
            revert InsufficientInput();
        }

        if (btcTx.locktime != 0) {
            revert InvalidLocktime();
        }

        // Recover the callers unique bitcoin deposit address
        bytes memory convertedBtcAddress = SovaBitcoin.convertToBtcAddress(msg.sender);

        if (keccak256(convertedBtcAddress) != keccak256(bytes(btcTx.outputs[voutIndex].addr))) {
            revert InvalidOutput(btcTx.outputs[voutIndex].addr, string(convertedBtcAddress));
        }

        return btcTx;
    }

    /**
     * @notice Calls the Vault Spend precompile to construct and broadcast a Bitcoin transaction
     *
     * @param caller            The address of the caller (EVM address)
     * @param amount            The amount of satoshis to withdraw
     * @param btcGasLimit       Specified gas limit for the Bitcoin transaction (in satoshis)
     * @param btcBlockHeight    The current BTC block height. This is used to source spendable Bitcoin UTXOs
     * @param dest              The destination Bitcoin address (bech32)
     *
     * @return btcTxid          Txid of the constructed Bitcoin transaction
     */
    function vaultSpend(address caller, uint64 amount, uint64 btcGasLimit, uint64 btcBlockHeight, string calldata dest)
        internal
        returns (bytes32 btcTxid)
    {
        bytes memory inputData = abi.encode(caller, amount, btcGasLimit, btcBlockHeight, dest);

        (bool success, bytes memory returndata) = VAULT_SPEND_PRECOMPILE_ADDRESS.call(inputData);
        if (!success) revert PrecompileCallFailed();

        return bytes32(returndata);
    }
}

Custom Smart Contract Example

Decode a Bitcoin Transaction

// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import {SovaBitcoin} from "@sova-network/src/lib/SovaBitcoin.sol";

contract BitcoinDecodeTx {
    function broadcastBitcoinTx(bytes memory rawTransaction) public returns (bytes32) {
        SovaBitcoin.BitcoinTx memory btcTx = SovaBitcoin.decodeBitcoinTx(rawTransaction);
        
        return btcTx.txid;
    }
}

Last updated