For Solidity Developers
Referenced Libraries:
Foundry Solady SovaBitcoin.sol
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);
}
}
```
Last updated