Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 8 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,27 @@
# Sova Contracts

This repository contains the predeploy contracts for the Sova Network.
This repository contains the predeploy contracts for the Sova Network. This repo serves as the source of truth for on chain deployments.

The Sova Network enables smart contract to directly interact with the Bitcoin blockchain. This interaction is done through the use of custom precompiles and predeployed contracts. This feature set allows smart contract to do things like broadcast transactions, decode payloads, verify signatures, get block height and more!
The Sova Network enables smart contracts to directly interact with the Bitcoin blockchain. Smart contract interaction is done through the use of custom precompiles and predeployed contracts. These features allow smart contracts to safely broadcast BTC transactions, decode BTC payloads, get BTC block height. As the network matures this feature set will continue to expand.

## Details

The Sova precompiles provide built-in Bitcoin transaction validation, broadcast capabilities, and UTXO management with safeguards against double-spending and replay attacks. These features power the SovaBTC.sol predeploy contract and are available to any developer through the SovaBitcoin.sol library. Our goal is to make it as easy as possible to add native Bitcoin functionality to your Sova smart contracts.

## Contracts
- **SovaL1Block** (`0x2100000000000000000000000000000000000015`) - Bitcoin state tracking
- **SovaBTC** (`0x2100000000000000000000000000000000000020`) - Bitcoin-backed ERC20 token
- **SovaBitcoin** - Library for Bitcoin precompile interactions
- **UBTC20** - Abstract base contract extending ERC20 with pending transaction states and slot locking. Prevents transfers during pending Bitcoin operations and handles deferred accounting for cross-chain finalization.

## Build

```shell
# Build the project
forge build
```
- **SovaL1Block** `0x2100000000000000000000000000000000000015`
- **SovaBTC** `0x2100000000000000000000000000000000000020`
- **SovaBitcoin** - Library for Bitcoin precompile interactions.
- **UBTC20** - Base contract extending ERC20 with pending transaction states and Bitcoin finality. Prevents transfers during pending Bitcoin operations and handles deferred accounting for cross-chain finalization.

## Deployed Bytecode verification

Generate the deployed byte code locally to verify the predeploy contract code used on the Sova Network.

```shell
# uBTC.sol
forge inspect src/UBTC.sol:UBTC deployedBytecode
# SovaBTC.sol
forge inspect src/SovaBTC.sol:SovaBTC deployedBytecode

# SovaL1Block.sol
forge inspect src/SovaL1Block.sol:SovaL1Block deployedBytecode
Expand Down
208 changes: 133 additions & 75 deletions src/SovaBTC.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,24 @@ import "./UBTC20.sol";
* @author Sova Labs
*
* Bitcoin meets ERC20. Bitcoin meets composability.
*
* @notice Uses sol compiler 0.8.15 for compatibility with Optimism Bedrock contracts
* that Sova Network used for chain artifacts creation.
* Ref: https://github.com/SovaNetwork/optimism/tree/op-deployer-v0.3.3-mainnet
*/
contract SovaBTC is ISovaBTC, UBTC20, Ownable, ReentrancyGuard {
/// @notice Minimum deposit amount in satoshis
uint64 public minDepositAmount;

/// @notice Maximum deposit amount in satoshis
uint64 public maxDepositAmount;

/// @notice Maximum gas limit amount in satoshis
uint64 public maxGasLimitAmount;

/// @notice Pause state of the contract
bool private _paused;

/// @notice Mapping to track Bitcoin txids that have been used for deposits
mapping(bytes32 => bool) private usedTxids;

/// @notice Mapping to track authorized withdraw signers
mapping(address => bool) public withdrawSigners;

error InsufficientDeposit();
error InsufficientInput();
error InsufficientAmount();
Expand All @@ -53,38 +54,73 @@ contract SovaBTC is ISovaBTC, UBTC20, Ownable, ReentrancyGuard {
error TransactionAlreadyUsed();
error PendingDepositExists();
error PendingWithdrawalExists();
error PendingUserWithdrawalRequestExists();
error UnauthorizedWithdrawSigner();
error SignerAlreadyExists();
error SignerDoesNotExist();

event Deposit(address caller, bytes32 txid, uint256 amount);
event Withdraw(address caller, bytes32 txid, uint256 amount);
event WithdrawSignaled(
address indexed user, uint256 amount, uint64 btcGasBid, uint64 operatorFee, string destination
);
event Withdraw(address user, bytes32 txid, uint256 totalAmount);
event MinDepositAmountUpdated(uint64 oldAmount, uint64 newAmount);
event MaxDepositAmountUpdated(uint64 oldAmount, uint64 newAmount);
event MaxGasLimitAmountUpdated(uint64 oldAmount, uint64 newAmount);
event ContractPausedByOwner(address indexed account);
event ContractUnpausedByOwner(address indexed account);
event WithdrawSignerAdded(address indexed signer);
event WithdrawSignerRemoved(address indexed signer);

constructor() Ownable() {
_initializeOwner(msg.sender);

minDepositAmount = 10_000; // 10,000 sat = 0.0001 BTC

_paused = false;

// Set initial withdrawal signer
withdrawSigners[0xd94FcA65c01b7052469A653dB466cB91d8782125] = true;
emit WithdrawSignerAdded(0xd94FcA65c01b7052469A653dB466cB91d8782125);
}

/// Modifiers

modifier whenNotPaused() {
_whenNotPaused();
_;
}

function _whenNotPaused() internal view {
if (_paused) {
revert ContractPaused();
}
_;
}

modifier whenPaused() {
_whenPaused();
_;
}

function _whenPaused() internal view {
if (!_paused) {
revert ContractNotPaused();
}
_;
}

constructor() Ownable() {
_initializeOwner(msg.sender);
modifier onlyWithdrawSigner() {
_onlyWithdrawSigner();
_;
}

minDepositAmount = 10_000; // (starts at 10,000 sats)
maxDepositAmount = 100_000_000_000; // (starts at 1000 BTC = 100 billion sats)
maxGasLimitAmount = 50_000_000; // (starts at 0.5 BTC = 50,000,000 sats)
_paused = false;
function _onlyWithdrawSigner() internal view {
if (!withdrawSigners[msg.sender]) {
revert UnauthorizedWithdrawSigner();
}
}

/// External

function isPaused() external view returns (bool) {
return _paused;
}
Expand Down Expand Up @@ -126,9 +162,6 @@ contract SovaBTC is ISovaBTC, UBTC20, Ownable, ReentrancyGuard {
if (amount < minDepositAmount) {
revert DepositBelowMinimum();
}
if (amount > maxDepositAmount) {
revert DepositAboveMaximum();
}

// Validate the BTC transaction and extract metadata
SovaBitcoin.BitcoinTx memory btcTx = SovaBitcoin.isValidDeposit(signedTx, amount, voutIndex);
Expand All @@ -152,25 +185,23 @@ contract SovaBTC is ISovaBTC, UBTC20, Ownable, ReentrancyGuard {
}

/**
* @notice Withdraws Bitcoin by burning uBTC tokens. Then triggering the signing, and broadcasting
* of a Bitcoin transaction.
* @notice Signals a withdrawal request by saving the withdrawal request.
*
* @dev For obvious reasons the UBTC_SIGN_TX_BYTES precompile is a sensitive endpoint. It is enforced
* in the execution client that only this contract can all that precompile functionality.
* @dev The hope is that in the future more SIGN_TX_BYTES endpoints can be added to the network
* and the backends are controlled by 3rd party signer entities.
* @dev The user must have enough sovaBTC to cover the amount + gas limit + operatorFee fee.
*
* @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)
* @param amount The amount of satoshis to withdraw (excluding gas)
* @param btcGasLimit The gas limit bid for the Bitcoin transaction in satoshis
* @param operatorFee The fee offered to the operator for processing the withdrawal in satoshis
* @param dest The Bitcoin address to send the withdrawn BTC to
*/
function withdraw(uint64 amount, uint64 btcGasLimit, uint64 btcBlockHeight, string calldata dest)
function signalWithdraw(uint64 amount, uint64 btcGasLimit, uint64 operatorFee, string calldata dest)
external
nonReentrant
whenNotPaused
{
// Input validation
if (bytes(dest).length == 0) {
revert EmptyDestination();
}

if (amount == 0) {
revert ZeroAmount();
}
Expand All @@ -179,26 +210,61 @@ contract SovaBTC is ISovaBTC, UBTC20, Ownable, ReentrancyGuard {
revert ZeroGasLimit();
}

if (btcGasLimit > maxGasLimitAmount) {
revert GasLimitTooHigh();
}

uint256 totalRequired = amount + btcGasLimit;
uint256 totalRequired = amount + btcGasLimit + operatorFee;
if (balanceOf(msg.sender) < totalRequired) {
revert InsufficientAmount();
}

if (_pendingWithdrawals[msg.sender].amount > 0) revert PendingWithdrawalExists();
// check if user already has a pending withdrawal
if (_pendingWithdrawals[msg.sender].amount > 0) {
revert PendingWithdrawalExists();
}

// Store the withdraw request
_pendingUserWithdrawRequests[msg.sender] = UserWithdrawRequest({
amount: amount, btcGasLimit: btcGasLimit, operatorFee: operatorFee, destination: dest
});

emit WithdrawSignaled(msg.sender, amount, btcGasLimit, operatorFee, dest);
}

/**
* @notice Processes a withdrawal signal by broadcasting a signed Bitcoin transaction.
*
* @dev Only authorized withdraw signers can call this function.
*
* @param user The address of the user who signaled the withdrawal
* @param signedTx The signed Bitcoin transaction to broadcast
*/
function withdraw(address user, bytes calldata signedTx, uint64 amount, uint64 btcGasLimit, uint64 operatorFee)
external
whenNotPaused
onlyWithdrawSigner
{
// Check if user has a pending withdrawal already
if (_pendingWithdrawals[user].amount > 0) revert PendingWithdrawalExists();

// decode signed tx so that we know it is a valid bitcoin tx
SovaBitcoin.BitcoinTx memory btcTx = SovaBitcoin.decodeBitcoinTx(signedTx);

uint256 totalAmount = amount + uint256(btcGasLimit) + uint256(operatorFee);

if (balanceOf(user) < totalAmount) revert InsufficientAmount();

// Track pending withdrawal
_setPendingWithdrawal(msg.sender, totalRequired);
_setPendingWithdrawal(user, totalAmount);

// Call Bitcoin precompile to construct the BTC tx and lock the slot
bytes32 btcTxid = SovaBitcoin.vaultSpend(msg.sender, amount, btcGasLimit, btcBlockHeight, dest);
// Clear the withdraw request
delete _pendingUserWithdrawRequests[user];

// Broadcast the signed BTC tx
SovaBitcoin.broadcastBitcoinTx(signedTx);

emit Withdraw(msg.sender, btcTxid, amount);
emit Withdraw(user, btcTx.txid, totalAmount);
}

/// Admin

/**
* @notice Admin function to burn tokens from a specific wallet
*
Expand All @@ -215,63 +281,55 @@ contract SovaBTC is ISovaBTC, UBTC20, Ownable, ReentrancyGuard {
* @param _minAmount New minimum deposit amount in satoshis
*/
function setMinDepositAmount(uint64 _minAmount) external onlyOwner {
if (_minAmount >= maxDepositAmount) {
revert InvalidDepositLimits();
}

uint64 oldAmount = minDepositAmount;
minDepositAmount = _minAmount;

emit MinDepositAmountUpdated(oldAmount, _minAmount);
}

/**
* @notice Admin function to set the maximum deposit amount
*
* @param _maxAmount New maximum deposit amount in satoshis
* @notice Admin function to pause the contract
*/
function setMaxDepositAmount(uint64 _maxAmount) external onlyOwner {
if (_maxAmount <= minDepositAmount) {
revert InvalidDepositLimits();
}

uint64 oldAmount = maxDepositAmount;
maxDepositAmount = _maxAmount;
function pause() external onlyOwner whenNotPaused {
_paused = true;

emit MaxDepositAmountUpdated(oldAmount, _maxAmount);
emit ContractPausedByOwner(msg.sender);
}

/**
* @notice Admin function to set the maximum gas limit amount
*
* @param _maxGasLimitAmount New maximum gas limit amount in satoshis
* @notice Admin function to unpause the contract
*/
function setMaxGasLimitAmount(uint64 _maxGasLimitAmount) external onlyOwner {
if (_maxGasLimitAmount == 0) {
revert ZeroAmount();
}

uint64 oldAmount = maxGasLimitAmount;
maxGasLimitAmount = _maxGasLimitAmount;
function unpause() external onlyOwner whenPaused {
_paused = false;

emit MaxGasLimitAmountUpdated(oldAmount, _maxGasLimitAmount);
emit ContractUnpausedByOwner(msg.sender);
}

/**
* @notice Admin function to pause the contract
* @notice Admin function to add a withdraw signer
*
* @param signer The address to add as a withdraw signer
*/
function pause() external onlyOwner whenNotPaused {
_paused = true;
function addWithdrawSigner(address signer) external onlyOwner {
if (withdrawSigners[signer]) {
revert SignerAlreadyExists();
}

emit ContractPausedByOwner(msg.sender);
withdrawSigners[signer] = true;
emit WithdrawSignerAdded(signer);
}

/**
* @notice Admin function to unpause the contract
* @notice Admin function to remove a withdraw signer
*
* @param signer The address to remove as a withdraw signer
*/
function unpause() external onlyOwner whenPaused {
_paused = false;
function removeWithdrawSigner(address signer) external onlyOwner {
if (!withdrawSigners[signer]) {
revert SignerDoesNotExist();
}

emit ContractUnpausedByOwner(msg.sender);
withdrawSigners[signer] = false;
emit WithdrawSignerRemoved(signer);
}
}
Loading