Sova Docs
  • Sova Developer Hub
  • Documentation
    • Intro
    • How It Works
    • Node Design & Architecture
    • Sova Whitepaper
    • Network Info
  • Developers
    • Contributing
    • For Frontend Developers
    • For Solidity Developers
    • Bitcoin Precompiles
    • Double Spend Protection
  • Node Operators
    • Dev Node
  • Community & Support
    • Frequently Asked Questions (FAQ)
Powered by GitBook
On this page
  • Intro
  • Precompile Library
  • Examples
  1. Developers

For Solidity Developers

PreviousFor Frontend DevelopersNextBitcoin Precompiles

Last updated 1 month ago

Referenced Libraries:


Intro

All EVM tooling works the same on Sova as any other network. Developers can use Foundry, Tenderly, web3 js libraries, etc...

Precompile Library

SovaBitcoin.sol

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

library SovaBitcoin {
    address private constant BTC_PRECOMPILE = address(0x999);

    bytes4 private constant BROADCAST_LEADING_BYTES = 0x00000001;
    bytes4 private constant DECODE_LEADING_BYTES = 0x00000002;
    bytes4 private constant CHECKSIG_LEADING_BYTES = 0x00000003;
    bytes4 private constant ADDRESS_CONVERT_LEADING_BYTES = 0x00000004;
    bytes4 private constant CREATE_AND_SIGN_LEADING_BYTES = 0x00000005;

    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();

    function decodeBitcoinTx(bytes memory signedTx) internal view returns (BitcoinTx memory) {
        (bool success, bytes memory returndata) =
            BTC_PRECOMPILE.staticcall(abi.encodePacked(DECODE_LEADING_BYTES, signedTx));
        if (!success) revert PrecompileCallFailed();
        return abi.decode(returndata, (BitcoinTx));
    }

    function checkSignature(bytes calldata signedTx) internal view returns (bool) {
        (bool success,) = BTC_PRECOMPILE.staticcall(abi.encodePacked(CHECKSIG_LEADING_BYTES, signedTx));
        return success;
    }

    function convertEthToBtcAddress(address ethAddress) internal returns (bytes memory) {
        (bool success, bytes memory returndata) =
            BTC_PRECOMPILE.call(abi.encodePacked(ADDRESS_CONVERT_LEADING_BYTES, ethAddress));
        if (!success) revert PrecompileCallFailed();
        return returndata;
    }

    function broadcastBitcoinTx(bytes memory signedTx) internal {
        (bool success,) = BTC_PRECOMPILE.call(abi.encodePacked(BROADCAST_LEADING_BYTES, signedTx));
        if (!success) revert PrecompileCallFailed();
    }

    function createAndSignBitcoinTx(address signer, uint64 amount, uint64 blockHeight, string memory destinationAddress)
        internal
        returns (bytes memory)
    {
        bytes memory inputData =
            abi.encode(CREATE_AND_SIGN_LEADING_BYTES, signer, amount, blockHeight, destinationAddress);

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

        return returndata;
    }
}

Examples

Broadcast a Bitcoin Transaction

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

import "./SovaBitcoin.sol";

contract BitcoinSender {
    function broadcastBitcoinTx(bytes memory rawTransaction) public returns (bytes32) {
        // Decode the transaction first to get the txid
        SovaBitcoin.BitcoinTx memory btcTx = SovaBitcoin.decodeBitcoinTx(rawTransaction);
        
        // Broadcast the transaction
        SovaBitcoin.broadcastBitcoinTx(rawTransaction);
        
        return btcTx.txid;
    }
}

Bitcoin Vault

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

import "solady/auth/Ownable.sol";
import "solady/utils/ReentrancyGuard.sol";
import "./lib/SovaBitcoin.sol";

/**
 * WARNING: THIS CONTRACT IS UNAUDITED AND SHOULD NOT BE USED IN PRODUCTION
 * This code is unaudited and for demonstration purposes only.
 *
 * @title BTCVault
 * @dev A contract that exposes a BTC address for deposits and allows
 *      transfers if the caller is the smart contract owner.
 */
contract BTCVault is Ownable, ReentrancyGuard {
    // Fee in satoshis for transfers
    uint256 public transferFee = 500000; // 0.005 BTC
    
    // Mapping to track user deposits
    mapping(address => uint256) public deposits;
    
    // Events
    event Deposited(address indexed user, bytes32 txid, uint256 amount);
    event Transferred(address indexed user, bytes32 txid, uint256 amount, string destination);
    event FeeUpdated(uint256 oldFee, uint256 newFee);
    
    // Errors
    error InsufficientDeposit();
    error InsufficientInput();
    error InvalidOutput(string expected, string actual);
    error UnsignedInput();
    error InvalidLocktime();
    error BroadcastFailure();
    error AmountTooBig();
    
    constructor() Ownable() {
        _initializeOwner(msg.sender);
    }
    
    /**
     * @dev Returns the Bitcoin address associated with this contract
     */
    function getBTCAddress() public returns (bytes memory) {
        return SovaBitcoin.convertEthToBtcAddress(address(this));
    }
    
    /**
     * @dev Allows a user to deposit BTC by providing a signed Bitcoin transaction
     * @param amount Amount in satoshis being deposited
     * @param signedTx The signed Bitcoin transaction
     */
    function depositBTC(uint256 amount, bytes calldata signedTx) public {
        // Decode signed bitcoin tx
        SovaBitcoin.BitcoinTx memory btcTx = SovaBitcoin.decodeBitcoinTx(signedTx);
        
        // Validations
        if (amount >= type(uint64).max) {
            revert AmountTooBig();
        }
        
        if (btcTx.outputs.length < 1 || btcTx.outputs[0].value < amount) {
            revert InsufficientDeposit();
        }
        
        if (btcTx.inputs.length < 1) {
            revert InsufficientInput();
        }
        
        if (btcTx.locktime > block.timestamp) {
            revert InvalidLocktime();
        }
        
        // Recover this contract's bitcoin address from its ethereum address
        bytes memory contractBtcAddress = SovaBitcoin.convertEthToBtcAddress(address(this));
        
        // Check that this contract's bitcoin address is the same as the signed tx's output[0] address
        if (keccak256(contractBtcAddress) != keccak256(bytes(btcTx.outputs[0].addr))) {
            revert InvalidOutput(btcTx.outputs[0].addr, string(contractBtcAddress));
        }
        
        // Check if signature is valid and the inputs are unspent
        if (!SovaBitcoin.checkSignature(signedTx)) {
            revert UnsignedInput();
        }
        
        // Record the deposit
        deposits[msg.sender] += amount;
        
        // Broadcast signed btc tx
        SovaBitcoin.broadcastBitcoinTx(signedTx);
        
        emit Deposited(msg.sender, btcTx.txid, amount);
    }
    
    /**
     * @dev Allows a user to transfer BTC to another address
     * @param amount Amount in satoshis to transfer
     * @param btcBlockHeight Current Bitcoin block height
     * @param destination The Bitcoin address to send to
     */
    function transferBTC(uint64 amount, uint32 btcBlockHeight, string calldata destination) public onlyOwner nonReentrant {
        // Check if user has enough balance for both amount and fee
        uint256 totalRequired = uint256(amount) + transferFee;
        if (deposits[msg.sender] < totalRequired) {
            revert InsufficientDeposit();
        }
        
        // Deduct the amount from user's deposit
        deposits[msg.sender] -= totalRequired;
        
        // Create Bitcoin transaction 
        bytes memory signedTx = SovaBitcoin.createAndSignBitcoinTx(
            address(this), 
            amount, 
            btcBlockHeight, 
            destination
        );
        
        // Decode signed bitcoin tx
        SovaBitcoin.BitcoinTx memory btcTx = SovaBitcoin.decodeBitcoinTx(signedTx);
        
        // Broadcast signed BTC tx
        SovaBitcoin.broadcastBitcoinTx(signedTx);
        
        emit Transferred(msg.sender, btcTx.txid, amount, destination);
    }
    
    /**
     * @dev Allows the owner to update the transfer fee
     * @param newFee The new fee in satoshis
     */
    function updateTransferFee(uint256 newFee) public onlyOwner {
        uint256 oldFee = transferFee;
        transferFee = newFee;
        emit FeeUpdated(oldFee, newFee);
    }
} 
```
Foundry
Solady
SovaBitcoin.sol