Welcome
Welcome to the sBTC developer documentation.
Here you will find the most up-to-date description of what sBTC is, how it's implemented and how you can get involved.
For deep technical dive into the sBTC system in its entirety, we recommend reading the proposed SIP as well.
Introduction to sBTC
To understand sBTC, we first need to understand the current limitations of Bitcoin (BTC).
Bitcoin is to date the most secure and decentralized blockchain. While Bitcoin is the largest cryptocurrency by market cap, comparatively few applications exist within the Bitcoin ecosystem. Developers interested in building applications for the Bitcoin community often find it difficult or impossible to implement their logic directly on the Bitcoin chain. Although Bitcoin has a simple scripting system built in, it lacks the expressiveness of many other smart contract languages.
sBTC is for:
- Bitcoin holders who want to participate in smart contracts.
- Developers who want to build applications on Bitcoin.
sBTC empowers developers to build applications on Bitcoin by bridging Bitcoin and Stacks. We achieve this by introducing a fungible token (sBTC) on the Stacks blockchain. The token has the following properties:
- 1:1 redeemability. sBTC can always be exchanged 1:1 for BTC on the Bitcoin chain, as long as the Stacks blockchain is operational.
- Open membership. Anyone can participate in the sBTC protocol. No centralized entity maintains custody over any BTC in the protocol.
Other tokens which try to achieve the same end as sBTC are
While these tokens all achieve the same value as BTC, they maintain BTC reserves through trusted entities. sBTC is the only truly decentralized Bitcoin backed asset on Stacks.
How does sBTC work?
Bitcoin holders can do two things to interact with sBTC, deposit and withdraw. Both of these operations are controlled through special Bitcoin transactions.
To deposit BTC into sBTC, a Bitcoin holder would create a deposit transaction on the Bitcoin chain. This deposit transaction informs the protocol how much BTC the holder has deposited, and to which Stacks address the holder wishes to receive the sBTC. The sBTC system responds to the deposit transaction by minting sBTC to the given Stacks address.
To withdraw BTC, a Bitcoin holder creates a withdrawal transaction on the Bitcoin chain. This withdrawal transaction informs the protocol how much sBTC the holder wishes to withdraw, from which stacks address the sBTC should be withdrawn and which Bitcoin address should receive the withdrawn BTC. In response to this transaction, the sBTC system burns the requested amount of sBTC from the given Stacks address and fulfills the withdrawal by issuing a BTC payment to the given BTC address with the same amount.
The following diagram illustrates the deposit and withdrawal flows.
sequenceDiagram Actor Bitcoin holder participant Bitcoin Actor sBTC Protocol participant Stacks Bitcoin holder ->> Bitcoin: Deposit transaction Bitcoin -->> sBTC Protocol: Read sBTC operations sBTC Protocol ->> Stacks: Mint sBTC Bitcoin holder ->> Bitcoin: Withdrawal transaction Bitcoin -->> sBTC Protocol: Read sBTC operations sBTC Protocol ->> Stacks: Burn sBTC sBTC Protocol ->> Bitcoin: Fulfill withdrawal
Where to go next?
If you want to use sBTC as user, check out How to Deposit and Withdraw.
If you're a developer looking to build applications with sBTC, check out the Quickstart.
If you want to dive into the details and understand how sBTC achieves a secure open-membership wrapping of Bitcoin, look into the design documentation. A good start is the Architecture Overview
How to Deposit and Withdraw
Depositing and withdrawing sBTC can be done in three ways:
- Using the sBTC Bridge application.
- Using an sBTC enabled wallet.
- Using an app which integrates with sBTC natively.
This guide will walk you through how to deposit and withdraw sBTC using the sBTC Brdige application.
Preparation
First, make sure you have the Hiro Wallet browser extension installed.
Then, to begin your deposit or withdrawal, navigate to https://bridge.stx.eco/.
Once there, click the Settings
dropdown and make sure you're on the right network.
Thereafter, you should select the appropriate transaction mode:
- If you want to deposit BTC from your Hiro Wallet, select
OP_RETURN
. - If you want to deposit BTC from a custodial wallet, select
OP_DROP
.
With these settings in place, you may proceed to do your deposit or withdrawal.
How to Deposit
To deposit you will be prompted to enter
- Your bitcoin address to deposit from.
- A stacks address to receive the sBTC.
- The amount to deposit.
Once you have entered this information and continue, you will either be prompted to sign a transaction with the Hiro Wallet or receive a QR code to scan depending on your transaction mode.
When you have signed or paid to the QR code you'll get a link to follow your request on the Bitcoin chain. The sBTC should be minted shortly after your request is mined.
How to Withdraw
To withdraw you will be prompted to enter
- Your bitcoin address to receive the BTC which is also used to request the withdrawal.
- Your stacks address from which the sBTC should be withdrawn.
- The amount to withdraw.
You will then be prompted to sign a message payload with the Hiro Wallet to authenticate your request. Once the request is authenticated you will be prompted to either sign a bitcoin transaction or receive a QR code to scan depending on your transaction mode.
When you have signed or paid to the QR code you'll get a link to follow your request on the Bitcoin chain. The sBTC should be burned shortly after your request is mined. Thereafter, the system will wait until the sBTC burn is final before fulfilling your withdrawal on the Bitcoin chain. This may take up to 150 bitcoin blocks.
How to Participate as a Stacker
Participating as a Stacker in the Stacks Blockchain is an essential role to ensure the liveliness of sBTC. To become a Stacker, you must hold and temporarily lock STX, Stacks’ native currency, and support the network’s security and consensus. As a reward, you earn BTC.
There are multiple ways for you to stack and earn Bitcoin - either through an exchange, a non-custodial stacking service, or independently. The most appropriate choice depends on your crypto experience and the amount of STX you have at your disposal. For direct participation, Stacks holders need a dynamic minimum amount of STX (approximately 100,000k STX during the mainnet, but this amount fluctuates based on overall participation and supply. See the pox
endpoint of the Hiro API to get the minimum of the next cycle). If you don't meet this minimum, you can still participate by leveraging third-party Stacking delegation services. These services combine your holdings with others, allowing joint participation. A complete breakdown of the stacking mechanism can be found in SIP-007.
Below is a documentation guide that outlines the steps to participate as a Stacker in the Stacks blockchain network, along with the responsibilities and actions involved in the role. Options for stacking through an exchange a stacking service can be found on the stacks.co stacking page.
Stacking Through an Exchange
-
Choose an Exchange Select a reputable cryptocurrency exchange that offers Stacking services for STX.
-
Create an Account If you don't have an account on the chosen exchange, sign up for one. Complete the necessary verification procedures.
-
Deposit STX Deposit the desired amount of STX into your exchange account.
-
Navigate to Stacking Section Go to the Stacking section or menu within the exchange platform.
-
Select Stacking Options Choose the stacking options that suit your preferences, such as the duration and amount to be stacked.
-
Start Stacking Confirm the stacking process on the exchange platform. Your STX will be locked for the chosen stacking period, and you'll start earning rewards. The exchange will fulfill your signing obligations on your behalf according to their own configurations.
-
Monitor Stacking Rewards Keep track of your stacking rewards on the exchange platform. The rewards will typically be automatically credited to your account.
Stacking Through a Non-Custodial Stacking Service
-
Choose a Non-Custodial Stacking Service Research and select a reputable non-custodial Stacking stacking service that aligns with your preferences and goals.
-
Set Up a Stacks Wallet Ensure you have a compatible Stacks wallet that supports Stacking. Examples include the Stacks Wallet or other wallets that are Stacking-enabled.
-
Acquire STX Acquire the desired amount of STX to participate in the Stacking stacking service.
-
Register with the Stacking Service Follow the registration process for the chosen stacking service. Provide the necessary details, including your wallet address.
-
Delegate Your STX Delegate your STX holdings to the stacking service. This process allows the stacking service to participate in Stacking on your behalf while your STX remains under your control. The stacking service will fulfill your signing obligations on your behalf according to their own configurations.
-
Receive Stacking Rewards As part of the stacking service, you'll receive Stacking rewards proportionate to your contribution. The rewards will typically be automatically distributed to your wallet by the stacking service operator.
Independent Stacking
-
Set Up a Stacks Wallet Choose a compatible Stacks wallet that supports Stacking. Download and install the wallet on your device.
-
Acquire STX Acquire the dynamic minimum amount of STX required for independent Stacking. As of now, this amount is approximately 100,000k STX, but verify for any updates on this requirement.
-
Setup or Select a Signer To fulfill your obligation to validate and sign sBTC transactions, you must first select or setup a Signer
-
Register as a Stacker After setting up your wallet and acquiring the required STX, register as a stacker on the Stacks Blockchain. The registration process may vary based on wallet providers.
-
Start Stacking Once registered, your wallet will facilitate the process of participating in the Stacking consensus and validating transactions. You will earn STX rewards for your contributions to securing the network.
Conclusion
Stacking in the Stacks Blockchain is a rewarding way to participate in the network's consensus and earn BTC rewards. Choose the method that aligns with your preferences, and always prioritize security when participating in Stacking. Remember to research the chosen exchange or stacking service and stay informed about any updates or changes to the Stacking process.
Quickstart
If you are a developer looking to start building with sBTC, this is the place to start.
1. Familiarize yourself with sBTC
First, if you aren't familiar with what sBTC is or how it works, be sure to check out the sBTC Design documentation.
2. Learn about the Developer Release
The Developer Release is the very first version of sBTC, and is designed to provide a preview version of sBTC for developers to learn and experiment with.
3. Get setup with a local dev environment
Once you understand how it works and what the Developer Release is, it's time to get the sBTC development environment set up locally with the Local Setup Guide.
4. Learn sBTC development
Now it's time to get up and running with a complete full-stack sBTC-powered application. The End to End Tutorial will teach you how to build an app using Next, Clarity, Stacks.js, and sBTC from start to finish.
5. Keep Going
Now that you've got the basics down, you can refer to the deposit and withdrawal code examples for a quick reference on how to initiate deposits and withdrawals with Stacks.js and the Leather wallet.
Get Started with sBTC 0.1 on devnet
Local setup
Devnet is a setup with a local bitcoin node running regtest, a local stacks node running testnet and connecting to the local bitcoin node. In addition, explorers and api servers are setup to provide easy access to information.
Developers should use docker to setup all required services.
Here are the basic steps we'll need to follow to get everything running.
- Launch devnet.
- Deploy sbtc contract (happens automatically).
- Launch sBTC binary (Alpha romeo engine).
- Use sBTC web app (sBTC Bridge) or sbtc cli or your own app.
Requirements
Launch Devnet
We'll get sBTC up and running locally using the sBTC devenv. You'll need to make sure you have Docker and Docker Compose installed on your system.
First, make sure you have the sbtc
repo cloned locally.
Now let's get everything running. Make sure you are in the devenv
folder and run:
./build.sh
This first build will take a while to complete, but once that's done we just need to run everything with:
./up.sh
Let's check to make sure things are running by visiting the Stacks explorer and the Bridge app. The Stacks explorer will take a bit get up and running.
- http://127.0.0.1:3020/?chain=testnet&api=http://127.0.0.1:3999 (Stacks explorer)
- http://127.0.0.1:8083 (Bitcoin explorer)
- http://127.0.0.1:8080 (sBTC Bridge App)
Deploy Contracts
The deployment of the Clarity contracts asset.clar
and clarinet-bitcoin-mini.clar
happens during the launch of devenv.sbtcWalletAddress
Under the hood, there is a script utils/deploy_contracts.sh
that is used to deploy the contracts The local environment defines wallets that are already funded. The deployer wallet is used to deploy the contract using clarinet deployments.
Prepare Wallet
You can take a look at the config file at sbtc/devenv/sbtc/docker/config.json
to see the mnemonic that defines the taproot address that will be the contract owner for sBTC. This is the mnemonic that generates the address that will actually call the mint
function in the sBTC contract.
If you take a look at the sbtc/romeo/asset-contract/settings/Devnet.toml
you can see this mnemonic associated with a specific Stacks address. This will also be the address that will be used to deploy the actual contracts.
Download the Leather wallet (version 6.11.+) and import the deployer address mnemonic into it to load that account locally.
twice kind fence tip hidden tilt action fragile skin nothing glory cousin green tomorrow spring wrist shed math olympic multiply hip blue scout claw
You can see more default generated mnemonics for the devnet in the devenv
repo.
In Leather wallet, you see that Account 1 is the deployer. Nevertheless, you can use this account or Account 2 as a normal user.
Mint some BTC and sBTC
There are helper scripts to create and broadcast deposit and withdrawal requests. They are contained in the utils
folder of devenv:
- mint_btc.sh: funds the btc addresses with BTC. Add the number of blocks to mint for the two addresses as parameter.
- deposit.sh: deposits a random amount of less than 10,000 sats. sBTC will be received by the deployer stacks address.
- withdrawal.sh: withdraws a random amount of less than 2,000 sats.
Before running these, make sure the Stacks devnet is running by visiting the explorer at http://127.0.0.1:3020/transactions?chain=testnet&api=http://127.0.0.1:3999.
If you get an error, wait a few minutes and refresh. Once that page loads correctly you should be good to go.
If you are using the Leather wallet, make sure to use the same secret key as used in devnet (deployer wallet). If you are using a different secret key you'll want to run this again and make sure that this is mining to the same wallet you are going to use in your sBTC app. To do that, view the Bitcoin address displayed in Leather (make sure you are on Devnet) and add it to the mine_btc.sh
script at the end like this:
btc_address='bcrt1q3tj2fr9scwmcw3rq5m6jslva65f2rqjxfrjz47'
First, let's mine some BTC with ./utils/mine_btc.sh 200
. This will mine 200 BTC blocks and ensure our address (Account 1 and Account 2) is funded.
Next we can initiate a deposit, which will deposit a random amount of satoshis from our Bitcoin wallet (Account 2) into the sBTC threshold wallet, minting sBTC in the process.
We can do that with ./utils/deposit.sh
.
And finally, we can do the reverse, convert our sBTC back to BTC, with ./utils/withdraw.sh
, which will print the txid of the withdrawal transaction on completion.
Check the results on Stacks at our address: http://localhost:3020/address/ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM?chain=testnet&api=http://localhost:3999
Check the results on Bitcoin at the txs printed by the utility functions, eg. http://127.0.0.1:8083/tx/38089273be6ed7521261c3a3a7d1bd36bc67d97123c27263e880350fc519899d, replacing the txid parameter with the given tx id.
Next Steps
Congratulations! You are among the first people in the world to run sBTC. Now you're ready to actually build something. For that, head over to the End to End Tutorial and we'll build a simple full-stack Bitcoin lending application powered by sBTC.
Full Stacks sBTC Tutorial
Build a Basic DeFi Application using Next, Stacks.js, Clarity, and the sBTC Developer Release
If you are looking to start building full-stack applications with the sBTC Developer Release (sBTC DR), this is the place to start. We'll walk through the entire process of building a full-stack application utilizing sBTC from start to finish.
If you prefer a quicker introduction, check out the Quickstart, which will get you up to speed on the essentials of building with sBTC.
First, some housekeeping.
Prerequisites
In this tutorial, we'll be building a basic Bitcoin lending app called Lagoon. We'll be using Next for our frontend and the Developer Release of sBTC on Stacks for the smart contract functionality.
We'll be building a full-stack app in this tutorial, primarily working in JavaScript and Clarity.
You should have at least some familiarity with the following before starting:
- React
- Next
- Stacks
- sBTC
- Clarity
If you aren't familiar with React or Next for frontend development, I recommend Vercel's Next Foundations to get up to speed.
If you aren't familiar with Stacks, check out Stacks Academy and if you need an intro to building on Stacks, take a look at the Stacks Developer Quickstart.
To get you up to speed on sBTC, you can start by familiarizing yourself with sBTC from a high level.
Specifically, we're working with the Developer Release now, which is an early version of sBTC for developers to begin experimenting and building before the full version is released.
Finally, if you aren't familiar with Clarity, you can get the basics, which is enough for this tutorial, by going through this Clarity Crash Course.
You'll also want to make sure you have the Leather web wallet installed, as that is what we'll be using to interact with our application.
Now let's get started.
What We're Building
We're going to be building an app called Lagoon. Lagoon is a very basic Bitcoin lending application that allows you to borrow and lend BTC using Clarity smart contracts. We'll take full advantage of sBTC here and use it in the process.
One of the primary solutions sBTC brings to the world is to create a robust decentralized financial system on top of Bitcoin, rather than needing to go through centralized custodians or pay high fees on the L1.
Lagoon allows users to connect their wallet, convert their BTC to sBTC, and then take that sBTC and deposit it into a lending pool in order to earn interest on it.
This is by no means a production level borrowing and lending app, and is only meant to be used to illustrate how to get started with sBTC. Feel free to use it as a starting point in your projects, but just know that the code is only for demo purposes and should not be used in production.
Getting Set Up
Note: There are still some bugs being worked out with the testnet sBTC system, so we're going to use a local developer environment for this tutorial.
For this tutorial, we're going to get you set up with a local version of the sBTC DR. Although this does require a bit more setup time, it will pay off by making your development experience significantly faster.
So, before going any further, make sure you have sBTC set up locally by following the Local environment setup guide.
Once you're all set up, it's time to start building!
If at any time you get stuck, you can refer to the final code at the Lagoon repo.
Creating Our Front End
The first thing we need to do is create a new folder for our project, called lagoon
.
mkdir lagoon && cd lagoon
Inside that we'll initiate our Next project for our frontend.
npx create-next-app@latest
Use the following values when answering the setup questions:
- Name: frontend
- TypeScript: no
- ESLint: up to you
- Tailwind: yes
- src directory: yes
- App Router: yes
- Customize import alias: no
Now let's get our frontend created. Since this isn't a React/Next tutorial, I'll gloss over the boilerplate code.
First, we need to install the @stacks/connect
package as this is what we'll use to connect our wallet.
npm install @stacks/connect
Now, let's update our layout.js
file in frontend/src/app/layout.js
to get a Navbar and Connect Wallet component created.
"use client";
import "./globals.css";
import { Inter } from "next/font/google";
import React, { useState, useEffect } from "react";
import { AppConfig, UserSession } from "@stacks/connect";
import Navbar from "./components/Navbar";
const inter = Inter({ subsets: ["latin"] });
export default function RootLayout({ children }) {
const [userData, setUserData] = useState({});
const appConfig = new AppConfig();
const userSession = new UserSession({ appConfig });
useEffect(() => {
if (userSession.isSignInPending()) {
userSession.handlePendingSignIn().then((userData) => {
setUserData(userData);
});
} else if (userSession.isUserSignedIn()) {
setUserData(userSession.loadUserData());
}
}, []);
return (
<html lang="en">
<body className={inter.className}>
<div className="min-h-screen text-white bg-slate-800">
{userData !== undefined ? (
<>
<Navbar
userSession={userSession}
userData={userData}
setUserData={setUserData}
/>
{children}
</>
) : (
""
)}
</div>
</body>
</html>
);
}
Next up let's add our Navbar by creating a Navbar.js
file inside the src/app/components
directory.
"use client";
import Link from "next/link";
import ConnectWallet from "./ConnectWallet";
export default function Navbar({ userSession, userData, setUserData }) {
return (
<nav className="flex justify-between p-4 bg-slate-900">
<ul className="flex justify-center space-x-4 text-white">
<li>
<Link href="/" className="hover:text-orange-500">
Home
</Link>
</li>
<li>
<Link href="/lend" className="hover:text-orange-500">
Lend
</Link>
</li>
<li>
<Link href="/borrow" className="hover:text-orange-500">
Borrow
</Link>
</li>
<li>
<Link href="/deposit" className="hover:text-orange-500">
Deposit BTC
</Link>
</li>
<li>
<Link href="/withdraw" className="hover:text-orange-500">
Withdraw sBTC
</Link>
</li>
</ul>
{userData ? (
<ConnectWallet
userSession={userSession}
userData={userData}
setUserData={setUserData}
/>
) : (
""
)}
</nav>
);
}
Next we need to create the ConnectWallet.js
component. You can do that inside the src/app/components
directory.
Inside that file, we'll add the following.
import { showConnect } from "@stacks/connect";
import { StacksMocknet, StacksTestnet } from "@stacks/network";
export default function ConnectWallet({ userSession, userData, setUserData }) {
const connectWallet = () => {
showConnect({
userSession,
network: StacksTestnet,
appDetails: {
name: "BitLoan",
icon: "https://freesvg.org/img/bitcoin.png",
},
onFinish: () => {
window.location.reload();
},
onCancel: () => {
// handle if user closed connection prompt
},
});
};
const disconnectWallet = () => {
userSession.signUserOut(window.location.origin);
setUserData({});
};
return (
<button
className="px-4 py-2 font-bold text-white transition duration-500 ease-in-out rounded bg-gradient-to-r from-purple-500 to-pink-500 hover:from-pink-500 hover:to-orange-500"
onClick={() => {
userData.profile ? disconnectWallet() : connectWallet();
}}
>
{userData.profile ? "Disconnect" : "Connect Wallet"}
</button>
);
}
All we are doing here is providing a mechanism for the user to connect with a web wallet. Walking through how each piece of the authentication works is outside the scope of this sBTC tutorial. Refer to the Stacks Quickstart linked above if you are unsure of what is happening here.
Then, update your src/app/page.js
file to look like this.
export const metadata = {
title: "Lagoon",
description: "A decentralized Bitcoin lending application",
};
export default function Home() {
return (
<>
<h1 className="mt-8 text-4xl text-center">Lagoon</h1>
<p className="mt-4 text-center">
Decentralized lending and borrowing with sBTC.
</p>
</>
);
}
Now we're going to add each page and component to create a basic UI.
src/app/borrow/page.js
import BorrowForm from "../components/BorrowForm";
export const metadata = {
title: "Borrow",
description: "A decentralized Bitcoin lending application",
};
export default function Borrow() {
return (
<div className="min-h-screen text-white bg-gray-800">
<h2 className="my-6 text-3xl text-center">Borrow sBTC</h2>
<BorrowForm />
</div>
);
}
deposit/page.js
import DepositForm from "../components/DepositForm";
export const metadata = {
title: "Deposit",
description: "A decentralized Bitcoin lending application",
};
export default function Deposit() {
return (
<div className="min-h-screen text-white bg-gray-800">
<h2 className="my-6 text-3xl text-center">Deposit BTC to Mint sBTC</h2>
<DepositForm />
</div>
);
}
lend/page.js
import LendForm from "../components/LendForm";
export const metadata = {
title: "Lend",
description: "A decentralized Bitcoin lending application",
};
export default function Lend() {
return (
<div className="min-h-screen text-white bg-gray-800">
<h2 className="my-6 text-3xl text-center">Lend your sBTC</h2>
<LendForm />
</div>
);
}
withdraw/page.js
import WithdrawForm from "../components/WithdrawForm";
export const metadata = {
title: "Withdraw",
description: "A decentralized Bitcoin lending application",
};
export default function Withdraw() {
return (
<div className="min-h-screen text-white bg-gray-800">
<h2 className="my-6 text-3xl text-center">Withdraw sBTC to BTC</h2>
<WithdrawForm />
</div>
);
}
components/BorrowForm.js
export default function BorrowForm() {
return (
<form className="flex flex-col items-center space-y-4">
<input
type="number"
placeholder="Amount to borrow"
className="w-1/3 px-4 py-2 text-gray-300 bg-gray-700 rounded focus:outline-none focus:border-orange-500"
/>
<input
type="number"
placeholder="Collateral amount"
className="w-1/3 px-4 py-2 text-gray-300 bg-gray-700 rounded focus:outline-none focus:border-orange-500"
/>
<button
type="submit"
className="w-1/3 px-6 py-2 bg-orange-500 rounded hover:bg-orange-600 focus:outline-none"
>
Borrow sBTC
</button>
</form>
);
}
components/DepositForm.js
export default function DepositForm() {
return (
<form className="flex items-center justify-center space-x-4">
<input
type="number"
placeholder="Amount of BTC to deposit"
className="w-1/3 px-4 py-2 text-gray-300 bg-gray-700 rounded focus:outline-none focus:border-orange-500"
/>
<button
type="submit"
className="px-6 py-2 bg-orange-500 rounded hover:bg-orange-600 focus:outline-none"
>
Deposit BTC
</button>
</form>
);
}
components/LendForm.js
export default function LendForm() {
return (
<form className="flex items-center justify-center space-x-4">
<input
type="number"
placeholder="Amount to lend"
className="w-1/3 px-4 py-2 text-gray-300 bg-gray-700 rounded focus:outline-none focus:border-orange-500"
/>
<button
type="submit"
className="px-6 py-2 bg-orange-500 rounded hover:bg-orange-600 focus:outline-none"
>
Lend sBTC
</button>
</form>
);
}
components/WithdrawForm.js
export default function WithdrawForm() {
return (
<form className="flex items-center justify-center space-x-4">
<input
type="number"
placeholder="Amount of sBTC to withdraw"
className="w-1/3 px-4 py-2 text-gray-300 bg-gray-700 rounded focus:outline-none focus:border-orange-500"
/>
<button
type="submit"
className="px-6 py-2 bg-orange-500 rounded hover:bg-orange-600 focus:outline-none"
>
Withdraw to BTC
</button>
</form>
);
}
If you run npm run dev
from the frontend
folder you should see the UI display.
What we have here is a basic UI for converting BTC and sBTC, and borrowing and lending sBTC.
Now that we have our basic UI in place, let's add functionality one piece at a time.
Initiating a sBTC Deposit
The first thing we are going to do is create a component to initiate a sBTC deposit.
You should already be familiar with how sBTC works at a high level, but what we are going to be doing is constructing a custom Bitcoin transaction that will have all the data we need in order to successfully deposit it into the sBTC wallet and then mint our sBTC.
Remember that for the Developer Release, the system that actually does the minting is not the fully decentralized version, it is a centralized single binary, but for the purposes of interacting with it as an application developer, the interface will be very similar to the final version.
Recall that in order to mint sBTC to our Stacks address we need to deposit the amount of BTC we want to convert into the threshold signature wallet, and pass in what Stacks address we want the sBTC minted to in via the OP_RETURN data.
The protocol and sBTC Clarity contracts will handle the actual minting of the sBTC.
We can use the sBTC package to make constructing that transaction much easier, and then we can use Leather's API to broadcast it.
We'll start by installing the sBTC package.
npm install sbtc
Next we need to set up the context that will allow us to have our UserData
available everywhere.
Create the UserContext.js
file within the src
directory and put this content in it:
import React from "react";
export const UserContext = React.createContext();
This will allow us to read from this file and pull in our authenticated user data in any part of the app.
Let's now update the DepositForm.js
component.
"use client";
import { useState, useContext } from "react";
import {
DevEnvHelper,
sbtcDepositHelper,
TESTNET,
TestnetHelper,
WALLET_00,
WALLET_01,
} from "sbtc";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import * as btc from "@scure/btc-signer";
import { UserContext } from "../UserContext";
export default function DepositForm() {
const { userData } = useContext(UserContext);
const [satoshis, setSatoshis] = useState("");
const handleInputChange = (event) => {
setSatoshis(event.target.value);
};
const buildTransaction = async (e) => {
e.preventDefault();
const testnet = new TestnetHelper();
// const testnet = new DevEnvHelper();
// setting BTC address for devnet
// const bitcoinAccountA = await testnet.getBitcoinAccount(WALLET_00);
// const btcAddress = bitcoinAccountA.wpkh.address;
// const btcPublicKey = bitcoinAccountA.publicKey.buffer.toString();
// setting BTC address for testnet
const btcAddress = userData.profile.btcAddress.p2wpkh.testnet;
const btcPublicKey = userData.profile.btcPublicKey.p2wpkh;
let utxos = await testnet.fetchUtxos(btcAddress);
// If we are working via testnet
// get sBTC deposit address from bridge API
const response = await fetch(
"https://bridge.sbtc.tech/bridge-api/testnet/v1/sbtc/init-ui"
);
const data = await response.json();
const sbtcWalletAddress = data.sbtcContractData.sbtcWalletAddress;
// if we are working via devnet
// const sbtcWalletAccount = await testnet.getBitcoinAccount(WALLET_00);
// const sbtcWalletAddress = sbtcWalletAccount.tr.address;
const tx = await sbtcDepositHelper({
// comment this line out if working via devnet
network: TESTNET,
sbtcWalletAddress,
stacksAddress: userData.profile.stxAddress.testnet,
amountSats: satoshis,
feeRate: await testnet.estimateFeeRate("low"),
utxos,
bitcoinChangeAddress: btcAddress,
});
const psbt = tx.toPSBT();
const requestParams = {
publicKey: btcPublicKey,
hex: bytesToHex(psbt),
};
const txResponse = await window.btc.request("signPsbt", requestParams);
const formattedTx = btc.Transaction.fromPSBT(
hexToBytes(txResponse.result.hex)
);
formattedTx.finalize();
const finalTx = await testnet.broadcastTx(formattedTx);
console.log(finalTx);
};
return (
<form className="flex items-center justify-center space-x-4">
<input
type="number"
placeholder="Amount of BTC to deposit"
className="w-1/3 px-4 py-2 text-gray-300 bg-gray-700 rounded focus:outline-none focus:border-orange-500"
value={satoshis}
onChange={handleInputChange}
/>
<button
type="submit"
className="px-6 py-2 bg-orange-500 rounded hover:bg-orange-600 focus:outline-none"
onClick={buildTransaction}
>
Deposit BTC
</button>
</form>
);
}
Before we go through the code, a reminder that in order to mint sBTC, you initiate a deposit transaction via Bitcoin. So to mint sBTC from a UI like this, we need to construct and broadcast that transaction on the Bitcoin network. Then the sBTC binary will detect that transaction and mint the sBTC.
We need a way to tell the Bitcoin network how much sBTC we want to mint. That's easy, we just send that amount to the threshold wallet. And we need to tell it where to send the sBTC. To do that we need to pass our Stacks address to the OP_RETURN of the Bitcoin transaction.
It can get pretty complicated to construct this manually. Luckily, the sBTC library will do most of the heavy lifting.
Okay let's go through this step by step. First we are importing some packages we need in order to construct our Bitcoin transaction.
We're also using React's state and context APIs to set how many sats we are depositing and to get our userData
.
At the bottom you can see in our UI that we have a basic form where users enter how many sats they want to convert and call the buildTransaction
function to initiate that.
Let's dig into that function.
The very first thing we are doing is setting what network we are on with either the DevenvHelper
or the TestnetHelper
from the sbtc
package.
Here we have it configured to use either testnet or devnet, depending on which line we have commented out. If you have your local devenv
up and running, great, use that. Otherwise, you can use testnet as well, you'll just need to wait for transactions to confirm.
The next thing we need to do is get all of the UTXOs that belong to our currently logged in address. The sbtc
package helps us with that as well and we can pass in our authenticated address.
Again, this will be a slightly different process depending on if you are on testnet or devnet.
Bitcoin, unlike the accounts model of something like Ethereum, works off of a UTXO model.
UTXO stands for unspent transaction output, and the collection of UTXOs that are spendable by a specific Bitcoin address determines how much bitcoin an address has.
When we create new transactions, we need to construct the inputs (what is being sent) based on those UTXOs. This helper function gets all of our UTXOs and formats them the right way.
Next we are using the sBTC Bridge API in order to get the current threshold wallet we should be sending the BTC to.
Next it's time to actually build the transaction. To do that we need to pass i:
- What network we are using, imported from the
sbtc
package. This defaults todevnet
so you only need to pass this parameter if you are using something else. - The threshold wallet address we got above
- Our stacks address. This is where the sBTC will be minted to
- How many sats we want to deposit and convert to sBTC
- The fee to use, we can get this by using another helper from the
sbtc
package - Our utxos that we fetched
- And where to send the remaining Bitcoin change after the transaction (UTXOs can only be spent as a whole, so we need to create another transaction to send any remainder)
Then we need to covnert that transaction to a PSBT. In order to use the Leather wallet, our transaction needs to be a PSBT, which we then pass to the wallet, use the wallet to sign, and then use the sbtc
helper to broadcast it.
The next few lines are converting the transaction to the right format, calling the wallet API to sign it, and broadcasting it.
Then we simply log the transaction. After you confirm the transaction in the Leather wallet, you can view this transaction in a Bitcoin explorer. Wait a few minutes (or a few seconds on devnet) and you should see your sBTC minted in your wallet.
:::note If you run into an error about the input script not having a pubKey, make sure you are authenticated with the same account you are initiating the transaction from. :::
Alright now that we are successfully minting sBTC, we're going to switch gears a bit and build out a super simple Clarity contract to handle our borrowing and lending functionality.
Creating Our Lending Smart Contract
We're going to be creating our new smart contract inside the sBTC repo we have running.
Switch into sbtc/romeo/asset-contract
and run clarinet contract new lagoon
.
If you are working from testnet the easiest option will probably be to write and deploy your contract from the Hiro explorer sandbox.
We want to developer our smart contract in the same folder and environment as sBTC.
Please keep in mind that this is for demo purposes only. Real DeFi lending systems are much more complex and robust than this. Don't use this as a model for building a DeFi protocol, it's only meant to be a starting point for how to work with sBTC.
Now let's write it.
First we're going to set up the data variables we need.
;; Define the contract's data variables
(define-map deposits { owner: principal } { amount: uint })
(define-map loans principal { amount: uint, last-interaction-block: uint })
(define-data-var total-deposits uint u0)
(define-data-var total-loans uint u0)
(define-data-var pool-reserve uint u0)
(define-data-var loan-interest-rate uint u10) ;; Representing 10% interest rate
We're creating a few different things here. First, we're setting up some maps that define all of the deposits and loans currently in the protocol, assigning an owner and an amount. We also have the last-interaction-block
field that will help us calculate how much interest is owed to a lender.
Next up we have a few basic variables defining how many deposits and loans we have in total, the pool reserve, which is the total interest paid by borrowers, and the interest rate, which we are hardcoding here for simplicity.
Now let's write our deposit function. This function will allow users to deposit sBTC into the protocol in order to generate interest.
(define-public (deposit (amount uint))
(let (
(current-balance (default-to u0 (get amount (map-get? deposits { owner: tx-sender }))))
)
(try! (contract-call? .asset transfer amount tx-sender (as-contract tx-sender) none))
(map-set deposits { owner: tx-sender } { amount: (+ current-balance amount) })
(var-set total-deposits (+ (var-get total-deposits) amount))
(ok true)
)
)
Let's walk through this. We are declaring a public function that takes in a uint
called amount
that will represent how many sats the user wants to deposit into the protocol.
Next we are using let
to get the current balance of the depositor. If they have never made a deposit and don't have an entry in the map, we default to 0.
Next up we are initiating a try!
call and calling the transfer
function from the sBTC contract, .asset
in this case. We are transferring the corresponding amount of sBTC from ourselves into the Lagoon contract.
If that is successful, we then update our deposits
map to add the amount we just deposited.
Finally, we update the total-deposits
as well and return true
.
Next up, let's write the borrow
function.
(define-public (borrow (amount uint))
(let (
(user-deposit (default-to u0 (get amount (map-get? deposits { owner: tx-sender }))))
(allowed-borrow (/ user-deposit u2))
(current-loan-details (default-to { amount: u0, last-interaction-block: u0 } (map-get? loans tx-sender )))
(accrued-interest (calculate-accrued-interest (get amount current-loan-details) (get last-interaction-block current-loan-details)))
(total-due (+ (get amount current-loan-details) (unwrap! accrued-interest (err u8))))
(new-loan (+ total-due amount))
)
(asserts! (<= amount allowed-borrow) (err u7))
(try! (contract-call? .asset transfer amount (as-contract tx-sender) tx-sender none))
(map-set loans tx-sender { amount: new-loan, last-interaction-block: block-height })
(ok true)
)
)
Again here we are using let
to retrieve and set the amount this user currently has in the pool, defaulting to 0.
Next we are getting the number that this user is allowed to borrow. Borrowers can only borrow up to 50% of their collateral.
Next we are checking to see how many blocks it has been since they last interacted with the protocol. We set this to keep track of interest accrual. We use the calculate-accrued-interest
function (which we'll implement in a moment) to calculate how much they owe and set that as well.
Next we are checking to make sure that the depositor is allowed to borrow and that the pool has enough liquidity to cover the new loan.
Then we transfer the sBTC from the contract to the borrower.
Finally we update all of our variables to include the new amount. Note that when we update these variables, we are taking interest into account. In this case, when this borrower repays this loan, they will repay the original amount plus the 10% interest.
Next up we have the repay function.
;; Users can repay their sBTC loans
(define-public (repay (amount uint))
(let (
(current-loan-details (default-to { amount: u0, last-interaction-block: u0 } (map-get? loans tx-sender)))
(accrued-interest (calculate-accrued-interest (get amount current-loan-details) (get last-interaction-block current-loan-details)))
(total-due (+ (get amount current-loan-details) (unwrap! accrued-interest (err u8))))
)
(asserts! (>= total-due amount) (err u4))
(try! (contract-call? .asset transfer amount tx-sender (as-contract tx-sender) none))
(map-set loans tx-sender { amount: (- total-due amount), last-interaction-block: block-height })
(var-set total-loans (- (var-get total-loans) amount))
(ok true)
)
)
This is very similar to the other functions. In this case the borrower is simply paying back their loan with the included interest. Remember that the interest was calculated when they initially borrowed, so we don't need to include it here.
Now let's write the function that allows lenders to claim their yield.
(define-public (claim-yield)
(let (
(user-deposit (default-to u0 (get amount (map-get? deposits { owner: tx-sender }))))
(yield-amount (/ (* (var-get pool-reserve) user-deposit) (var-get total-deposits)))
)
(try! (as-contract (contract-call? .asset transfer yield-amount (as-contract tx-sender) tx-sender none)))
(var-set pool-reserve (- (var-get pool-reserve) yield-amount))
(ok true)
)
)
First we get the user's deposits and calculate how much interest they are owed based on their proportion of the pool reserve.
Then we simply transfer that amount to them and update the pool reserve with the new amount.
And finally let's implement the function to calculate interest.
(define-private (calculate-accrued-interest (principal uint) (start-block uint))
(let (
(elapsed-blocks (- block-height start-block))
(interest (/ (* principal (var-get loan-interest-rate) elapsed-blocks) u10000))
)
(asserts! (not (is-eq start-block u0)) (ok u0))
(ok interest)
)
)
Here we are using the amount of elapsed blocks to calculate interest that accrues linearly across blocks. If the user has never taken out a loan, the interest is set to 0.
There is a lot of functionality missing here that we would need in a real DeFi protocol including liquidation, compound interest, withdrawal functionality for depositors, and a lot of controls to ensure the system can't be gamed. The purpose of this tutorial is to show you how to use sBTC, not build a full-fledged DeFi product. As such, we'll keep the functionality basic, as that's enough to understand how to utilize sBTC and the role it would play in a DeFi application.
Before we can interact with our contract, we need to deploy it. To do that, we can create a new deployment plan with Clarinet.
To do this, simply modify the default.devnet.yaml
file in deployments and add your new contract to the list of contracts to publish.
- contract-publish:
contract-name: lagoon
expected-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
cost: 38610
path: contracts/lagoon.clar
anchor-block-only: true
clarity-version: 2
Now you'll need to restart your environment and call the deploy_contracts.sh
helper again. Refer back to the devnet setup guide if this is confusing.
If you deployed to testnet, you don't need to worry about this part.
Now that we have a basic smart contract in place, let's build the UI to actually interact with it.
Creating the Lend UI
We're going to be build out the Lend form first. Go ahead and replace the contents of the LendForm.js
file with the following, then we'll go through it line by line.
"use client";
import React, { useState } from "react";
import { uintCV, PostConditionMode } from "@stacks/transactions";
import { openContractCall } from "@stacks/connect";
import { StacksMocknet, StacksTestnet } from "@stacks/network";
export default function LendForm() {
const [amount, setAmount] = useState(0);
const handleDeposit = async () => {
const functionArgs = [
uintCV(amount), // Convert the amount to uintCV
];
const contractAddress = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM"; // Replace with your contract address
const contractName = "lagoon"; // Replace with your contract name
const functionName = "deposit"; // Function for deposit
const options = {
contractAddress,
contractName,
functionName,
functionArgs,
network: new StacksTestnet(),
// network: new StacksMocknet(),
postConditionMode: PostConditionMode.Allow,
appDetails: {
name: "Lagoon",
icon: "https://freesvg.org/img/bitcoin.png", // You can provide an icon URL for your application
},
onFinish: (data) => {
console.log(data);
},
};
await openContractCall(options);
};
return (
<form
className="flex items-center justify-center space-x-4"
onSubmit={(e) => {
e.preventDefault();
handleDeposit();
}}
>
<input
type="number"
placeholder="Amount to lend"
value={amount}
onChange={(e) => setAmount(e.target.value)}
className="w-1/3 px-4 py-2 text-gray-300 bg-gray-700 rounded focus:outline-none focus:border-orange-500"
/>
<button
type="submit"
className="px-6 py-2 bg-orange-500 rounded hover:bg-orange-600 focus:outline-none"
>
Lend sBTC
</button>
</form>
);
}
Creating the Borrow UI
The borrow UI will be very similar. We're doing almost the exact same thing except renaming a couple things and calling the borrow
function instead of lend
.
"use client";
import React, { useState } from "react";
import { uintCV, PostConditionMode } from "@stacks/transactions";
import { openContractCall } from "@stacks/connect";
import { StacksMocknet, StacksTestnet } from "@stacks/network";
export default function BorrowForm() {
const [amount, setAmount] = useState(0);
const handleDeposit = async () => {
const functionArgs = [
uintCV(amount), // Convert the amount to uintCV
];
const contractAddress = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM"; // Replace with your contract address
const contractName = "lagoon"; // Replace with your contract name
const functionName = "borrow"; // Function for deposit
const options = {
contractAddress,
contractName,
functionName,
functionArgs,
network: new StacksTestnet(),
//network: new StacksMocknet(),
postConditionMode: PostConditionMode.Allow,
appDetails: {
name: "Lagoon",
icon: "https://freesvg.org/img/bitcoin.png", // You can provide an icon URL for your application
},
onFinish: (data) => {
console.log(data);
},
};
await openContractCall(options);
};
return (
<form
className="flex items-center justify-center space-x-4"
onSubmit={(e) => {
e.preventDefault();
handleDeposit();
}}
>
<input
type="number"
placeholder="Amount to borrow"
value={amount}
onChange={(e) => setAmount(e.target.value)}
className="w-1/3 px-4 py-2 text-gray-300 bg-gray-700 rounded focus:outline-none focus:border-orange-500"
/>
<button
type="submit"
className="px-6 py-2 bg-orange-500 rounded hover:bg-orange-600 focus:outline-none"
>
Borrow sBTC
</button>
</form>
);
}
Initiating a sBTC Withdrawal
Now we can mint sBTC, borrow sBTC, and lend out sBTC. Now if we want to convert our sBTC back to BTC, we need to withdraw it. We can do that with a very similar process as the deposit.
Add the following to the WithdrawForm.js
file.
"use client";
import { useState, useContext } from "react";
import {
DevEnvHelper,
sbtcWithdrawHelper,
sbtcWithdrawMessage,
TESTNET,
TestnetHelper,
} from "sbtc";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import * as btc from "@scure/btc-signer";
import { openSignatureRequestPopup } from "@stacks/connect";
import { UserContext } from "../UserContext";
import { StacksTestnet } from "@stacks/network";
export default function WithdrawForm() {
const { userData, userSession } = useContext(UserContext);
const [satoshis, setSatoshis] = useState("");
const [signature, setSignature] = useState("");
const handleInputChange = (event) => {
setSatoshis(event.target.value);
};
const signMessage = async (e) => {
e.preventDefault();
const message =
sbtcWithdrawMessage({
network: TESTNET,
amountSats: satoshis,
bitcoinAddress: userData.profile.btcAddress.p2wpkh.testnet,
});
openSignatureRequestPopup({
message,
userSession,
network: new StacksTestnet(),
onFinish: (data) => {
setSignature(data.signature);
},
});
};
const buildTransaction = async (e) => {
e.preventDefault();
const testnet = new TestnetHelper();
// const testnet = new DevEnvHelper();
let utxos = await testnet.fetchUtxos(
userData.profile.btcAddress.p2wpkh.testnet
);
// get sBTC deposit address from bridge API
const response = await fetch(
"https://bridge.sbtc.tech/bridge-api/testnet/v1/sbtc/init-ui"
);
const data = await response.json();
const tx = await sbtcWithdrawHelper({
network: TESTNET,
sbtcWalletAddress: data.sbtcContractData.sbtcWalletAddress,
bitcoinAddress: userData.profile.btcAddress.p2wpkh.testnet,
amountSats: satoshis,
signature,
feeRate: await testnet.estimateFeeRate("low"),
fulfillmentFeeSats: 2000,
utxos,
bitcoinChangeAddress: userData.profile.btcAddress.p2wpkh.testnet,
});
const psbt = tx.toPSBT();
const requestParams = {
publicKey: userData.profile.btcPublicKey.p2wpkh.testnet,
hex: bytesToHex(psbt),
};
const txResponse = await window.btc.request("signPsbt", requestParams);
const formattedTx = btc.Transaction.fromPSBT(
hexToBytes(txResponse.result.hex)
);
formattedTx.finalize();
const finalTx = await testnet.broadcastTx(formattedTx);
console.log(finalTx);
};
return (
<form className="flex items-center justify-center space-x-4">
<input
type="number"
placeholder="Amount of BTC to deposit"
className="w-1/3 px-4 py-2 text-gray-300 bg-gray-700 rounded focus:outline-none focus:border-orange-500"
value={satoshis}
onChange={handleInputChange}
/>
<button
className="px-6 py-2 bg-orange-500 rounded hover:bg-orange-600 focus:outline-none"
onClick={(e) => {
signature ? buildTransaction(e) : signMessage(e);
}}
>
{signature ? "Broadcast Withdraw Tx" : "Sign Withdraw Tx"}
</button>
</form>
);
}
There's a lot going on here, so let's break it down.
First we need to import quite a few different things from sbtc
and some stacks.js packages. We'll go over what these do as we use them.
Next we are pulling in our user data and the network we will be working with, testnet in this case.
Next up we have some similar state variables as before, and an addition one, signature.
When we initiate withdrawals, we first need to sign a Stacks message proving we are the owner of the account with the sBTC, then we can take that signature and broadcast it with our withdrawal request on the Bitcoin side.
That's what this signMessage
function is doing. We are generating our message in a specific format that the sBTC binary expects it, and then signing it, and saving that signature.
Once that signature is set, we can then broadcast our transaction.
We do that in a similar way to the deposit function, where we use a helper to build the withdrawal transaction, passing it all the data we need (including the signature we just generated) and using Leather to sign and broadcast it.
Once that is broadcasted successfully, you'll see the transaction ID logged and you can view it in a block explorer.
Wrapping Up
Congratulations! You just built a basic DeFi app powered by Bitcoin. sBTC is just a baby right now, and many more improvements will be made over the coming months as the system is fully fleshed out.
In the meantime, continue to explore what it's capable of and keep up with development by checking out sBTC.tech and Bitcoin Writes.
Initiating a Deposit
In order to create a deposit and convert our BTC to sBTC, we need to create and broadcast a Bitcoin transaction with the necessary data.
Below is an example code snippet for doing that using the Leather wallet in a Next.js app.
If you want to see how to do this in the context of a complete full-stack app, check out the tutorial, which this example was pulled from. We have it here for easy reference.
This example heavily relies on the sbtc
package. Documentation is in progress, but you can see the repo here and take a look at the test files to see examples of how to use it to construct transactions.
"use client";
import { useState, useContext } from "react";
import {
DevEnvHelper,
sbtcDepositHelper,
TESTNET,
TestnetHelper,
WALLET_00,
WALLET_01,
} from "sbtc";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import * as btc from "@scure/btc-signer";
import { UserContext } from "../UserContext";
export default function DepositForm() {
const { userData } = useContext(UserContext);
const [satoshis, setSatoshis] = useState("");
const handleInputChange = (event) => {
setSatoshis(event.target.value);
};
const buildTransaction = async (e) => {
e.preventDefault();
// Helper for working with various API and RPC endpoints and getting and processing data
// Change this depending on what network you are working with
const testnet = new TestnetHelper();
// const testnet = new DevEnvHelper();
// setting BTC address for devnet
// Because of some quirks with Leather, we need to pull our BTC wallet using the helper if we are on devnet
// const bitcoinAccountA = await testnet.getBitcoinAccount(WALLET_00);
// const btcAddress = bitcoinAccountA.wpkh.address;
// const btcPublicKey = bitcoinAccountA.publicKey.buffer.toString();
// setting BTC address for testnet
// here we are pulling directly from our authenticated wallet
const btcAddress = userData.profile.btcAddress.p2wpkh.testnet;
const btcPublicKey = userData.profile.btcPublicKey.p2wpkh;
let utxos = await testnet.fetchUtxos(btcAddress);
// If we are working via testnet
// get sBTC deposit address from bridge API
const response = await fetch(
"https://bridge.sbtc.tech/bridge-api/testnet/v1/sbtc/init-ui"
);
const data = await response.json();
const sbtcWalletAddress = data.sbtcContractData.sbtcWalletAddress;
// if we are working via devnet we can use the helper to get the sbtc wallet address, which is associated with the first wallet
// const sbtcWalletAccount = await testnet.getBitcoinAccount(WALLET_00);
// const sbtcWalletAddress = sbtcWalletAccount.tr.address;
const tx = await sbtcDepositHelper({
// comment this line out if working via devnet
network: TESTNET,
sbtcWalletAddress,
stacksAddress: userData.profile.stxAddress.testnet,
amountSats: satoshis,
// we can use the helper to get an estimated fee for our transaction
feeRate: await testnet.estimateFeeRate("low"),
// the helper will automatically parse through these and use one or some as inputs
utxos,
// where we want our remainder to be sent. UTXOs can only be spent as is, not divided, so we need a new input with the difference between our UTXO and how much we want to send
bitcoinChangeAddress: btcAddress,
});
// convert the returned transaction object into a PSBT for Leather to use
const psbt = tx.toPSBT();
const requestParams = {
publicKey: btcPublicKey,
hex: bytesToHex(psbt),
};
// Call Leather API to sign the PSBT and finalize it
const txResponse = await window.btc.request("signPsbt", requestParams);
const formattedTx = btc.Transaction.fromPSBT(
hexToBytes(txResponse.result.hex)
);
formattedTx.finalize();
// Broadcast it using the helper
const finalTx = await testnet.broadcastTx(formattedTx);
// Get the transaction ID
console.log(finalTx);
};
return (
<form className="flex items-center justify-center space-x-4">
<input
type="number"
placeholder="Amount of BTC to deposit"
className="w-1/3 px-4 py-2 text-gray-300 bg-gray-700 rounded focus:outline-none focus:border-orange-500"
value={satoshis}
onChange={handleInputChange}
/>
<button
type="submit"
className="px-6 py-2 bg-orange-500 rounded hover:bg-orange-600 focus:outline-none"
onClick={buildTransaction}
>
Deposit BTC
</button>
</form>
);
}
Initiating a Withdrawal
In order to create a withdrawal and convert our sBTC back to BTC, we need to create and broadcast a Bitcoin transaction with the necessary data in addition to signing a Stacks message to prove we own the sBTC we are trying to withdraw.
Below is an example code snippet for doing that using the Leather wallet in a Next.js app.
If you want to see how to do this in the context of a complete full-stack app, check out the tutorial, which this example was pulled from. We have it here for easy reference.
This example heavily relies on the sbtc
package. Documentation is in progress, but you can see the repo here and take a look at the test files to see examples of how to use it to construct transactions.
"use client";
import { useState, useContext } from "react";
import {
DevEnvHelper,
sbtcWithdrawHelper,
sbtcWithdrawMessage,
TESTNET,
TestnetHelper,
} from "sbtc";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import * as btc from "@scure/btc-signer";
import { openSignatureRequestPopup } from "@stacks/connect";
import { UserContext } from "../UserContext";
import { StacksTestnet } from "@stacks/network";
export default function WithdrawForm() {
const { userData, userSession } = useContext(UserContext);
const [satoshis, setSatoshis] = useState("");
const [signature, setSignature] = useState("");
const handleInputChange = (event) => {
setSatoshis(event.target.value);
};
const signMessage = async (e) => {
e.preventDefault();
// First we need to sign a Stacks message to prove we own the sBTC
// The sbtc paclage can help us format this
const message =
sbtcWithdrawMessage({
network: TESTNET,
amountSats: satoshis,
bitcoinAddress: userData.profile.btcAddress.p2wpkh.testnet,
});
// Now we can use Leather to sign that message
openSignatureRequestPopup({
message,
userSession,
network: new StacksTestnet(),
onFinish: (data) => {
// Here we set the signature
setSignature(data.signature);
},
});
};
const buildTransaction = async (e) => {
// Once the signature has been set, we can build and broadcast the transaction
e.preventDefault();
// Helper for working with various API and RPC endpoints and getting and processing data
// Change this depending on what network you are working with
const testnet = new TestnetHelper();
// const testnet = new DevEnvHelper();
// setting BTC address for devnet
// Because of some quirks with Leather, we need to pull our BTC wallet using the helper if we are on devnet
// const bitcoinAccountA = await testnet.getBitcoinAccount(WALLET_00);
// const btcAddress = bitcoinAccountA.wpkh.address;
// const btcPublicKey = bitcoinAccountA.publicKey.buffer.toString();
// setting BTC address for testnet
// here we are pulling directly from our authenticated wallet
const btcAddress = userData.profile.btcAddress.p2wpkh.testnet;
const btcPublicKey = userData.profile.btcPublicKey.p2wpkh;
let utxos = await testnet.fetchUtxos(btcAddress);
// If we are working via testnet
// get sBTC deposit address from bridge API
const response = await fetch(
"https://bridge.sbtc.tech/bridge-api/testnet/v1/sbtc/init-ui"
);
const data = await response.json();
const sbtcWalletAddress = data.sbtcContractData.sbtcWalletAddress;
// if we are working via devnet we can use the helper to get the sbtc wallet address, which is associated with the first wallet
// const sbtcWalletAccount = await testnet.getBitcoinAccount(WALLET_00);
// const sbtcWalletAddress = sbtcWalletAccount.tr.address;
const tx = await sbtcWithdrawHelper({
// comment this line out if working via devnet
network: TESTNET,
sbtcWalletAddress,
bitcoinAddress: btcAddress,
amountSats: satoshis,
signature,
feeRate: await testnet.estimateFeeRate("low"),
fulfillmentFeeSats: 2000,
utxos,
bitcoinChangeAddress: btcAddress,
});
const psbt = tx.toPSBT();
const requestParams = {
publicKey: btcPublicKey,
hex: bytesToHex(psbt),
};
const txResponse = await window.btc.request("signPsbt", requestParams);
const formattedTx = btc.Transaction.fromPSBT(
hexToBytes(txResponse.result.hex)
);
formattedTx.finalize();
const finalTx = await testnet.broadcastTx(formattedTx);
console.log(finalTx);
};
return (
<form className="flex items-center justify-center space-x-4">
<input
type="number"
placeholder="Amount of BTC to deposit"
className="w-1/3 px-4 py-2 text-gray-300 bg-gray-700 rounded focus:outline-none focus:border-orange-500"
value={satoshis}
onChange={handleInputChange}
/>
<button
className="px-6 py-2 bg-orange-500 rounded hover:bg-orange-600 focus:outline-none"
onClick={(e) => {
signature ? buildTransaction(e) : signMessage(e);
}}
>
{signature ? "Broadcast Withdraw Tx" : "Sign Withdraw Tx"}
</button>
</form>
);
}
The sBTC SDK
How to operate a signer
As a Stacker, you are responsible for ensuring the accuracy and integrity of incoming sBTC transactions. To satisfy this requirement and continue to earn BTC rewards, you must operate your own Signer or delegate your signing power to a third-party Signer. See the step-by-step guide to setting up and operating a Signer either by using the default Signer implementation or by utilizing the Signer SDK.
Developer Release
The sBTC Developer Release is a minimum-viable testnet deployment of sBTC and was decided to be released without the complexity of adding a decentralized set of signers to validate sBTC deposits/withdrawals.
Therefore our beloved Developer Release aka Version 0.1 Alpha Romeo only has one single signer that is operated by a central authority.
What is this
Draft documentation for sBTC Signer specifically for the Nakamoto release
The readers of this are assumed to be anyone who is looking to run a sBTC Signer with minimal custom configuration.
Disclaimer
Development is ongoing and changes to the Signer configuration should be expected. This document will be updated every sprint to ensure its accuracy.
Signing functionality will only become active and relevant on the Stacks mainnet at the release of the Nakamoto upgrade, currently expected in early 2024. Prospective signers and Stacking providers can contact signers@sbtc.tech for support and additional information.
Prerequisites
Rust - To install.
Accessible Stacks node - A Stacks node running the stackerDB instance which is used for signer communication and transaction monitoring and broadcasting.
Accessible Bitcoin node - A Bitcoin node used for transaction monitoring and broadcasting.
A Stacks Private Key - Identify your address as a Signer
Installing
Building from Source
If you wish to compile the default binary from source, follow the steps outlined below. Otherwise, download the binary directly (below)
- First, clone the Stacks sBTC mono repository:
git clone git@github.com:stacks-network/stacks-blockchain.git
- Next, navigate to the stacks-signer directory:
cd stacks-blockchain/stacks-signer
- Checkout the appropriate release branch you wish to use if you are not using the default main branch
git checkout master
- Compile the signer binary:
Note the binary path defaults to
target/release/stacks-signer
.
cargo build --release
Downloading the Binary
-
First, download the precompiled default TODO:NEED:LINK.
-
Untar the file
tar -xvf signer_binary.tar
-
Check Extracted Files: After running the untar command, the contents of the tar file should be extracted to the current directory. You should see the signer binary (stacks-signer) and the configuration file (signer.toml) listed among the extracted files.
-
Next, install the signer.
cargo install --path stacks-signer
Configuration
The signer takes a TOML config file with the following expected properties
Key | Required | Description |
---|---|---|
signer_private_key | true | Stacks private key of the signer, used for signing sBTC transactions. |
stacks_node_rpc_url | true | Stacks node RPC URL that points to a node running the stackerDB instance which is used for signer communication and transaction monitoring and broadcasting. |
bitcoin_node_rpc_url | true | Bitcoin node RPC URL used for transaction monitoring and broadcasting. |
stackerdb_event_endpoint | true | RPC endpoint for receiving events from StackerDB |
stackerdb_contract_id | false | StackerDB qualified contract ID for Signer communication. Defaults to "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb". |
network | false | One of ['Signet', 'Regtest', 'Testnet', 'Bitcoin'] . Defaults to Testnet |
signer_api_server_url | false | Url at which to host the signer api server for transaction monitoring. Defaults to "http://localhost:3000". |
auto_deny_block | false | Number of blocks before signing deadline to auto deny a transaction waiting for manual review. Defaults to 10. |
auto_approve_max_amount | false | Maximum amount of a transactions that will be auto approved |
auto_deny_addresses_btc | false | List of bitcoin addresses that trigger an auto deny |
auto_deny_addresses_stx | false | List of stx addresses that trigger an auto deny |
auto_deny_deadline_blocks | false | The number of blocks before deadline at which point the transaction will be auto denied. Default is 10 blocks. |
Example TOML file
# config.toml
# Mandatory fields
# Note: Replace 'MY_PRIVATE_KEY' with the actual private key value
signer_private_key = "MY_PRIVATE_KEY"
stacks_node_rpc_url = "http://localhost:9776"
bitcoin_node_rpc_url = "http://localhost:9777"
revealer_rpc_url = "http://locahost:9778"
# Optional fields
network = "Signet"
auto_approve_max_amount = 500000
auto_deny_addresses_btc = [
"BTC_ADDRESS_1",
"BTC_ADDRESS_2"
]
auto_deny_addresses_stx = [
"STX_ADDRESS"
]
auto_deny_deadline_blocks = 120
Running the binary
After installing and creating a config file to run the binary
stacks-signer --config conf/signer.toml
Monitor Transactions
The signer binary operates a web server/client and it can be navigated to by default at http://localhost:3000/ (unless otherwise specified from config). Here you can see pending transactions and manually review and sign transactions that cannot be automatically signed on your behalf. Note that manual review is triggered based on the options you have set in your configuration file.
Custom Signer Implementation
If you wish to have more fine-grained control of the Signer binary and its transaction signing logic, you may wish to take advantage of the [Signer SDK](TODO: LINK TO GITHUB REPO).
- Set Up a New Rust Project
To add a Signer library to your Rust project and create a main function that utilizes it, follow these step-by-step instructions:
If you don't have an existing Rust project, create one using Cargo, Rust's package manager and build tool:
cargo new my_signer
cd my_signer
Replace my_signer
with your desired project name.
- Add Signer Library to the
Cargo.toml
File
Open the Cargo.toml
file in your project directory and add the Signer library as a dependency under the [dependencies]
section.
[dependencies]
signer = "1.0.0"
Specify the appropriate version that you wish to use. Make sure to check the latest version available on crates.io.
- Import the Signer Library in Your Rust Code
In your main.rs
file (located in the src
folder by default), import the Signer library at the beginning of the file:
#![allow(unused)] fn main() { use signer::Signer; }
- Create a Main Function
Add the main function to your main.rs
file. This is where you'll utilize the Signer library to perform the required actions:
fn main() { // Initialize the signer with a private key let signer = Signer::new("your_private_key"); // Replace with the actual private key // Must serve web client to utilize manual review let _ = signer.serve_http("0.0.0.0", 3000); while let Ok(transaction) = signer.retrieve_pending_transaction() { // Trigger manual review for a specific address if transaction.recipient.to_string() == "mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC" { // Manually approve or deny a transaction let _ = signer.trigger_manual_review(transaction); } else if transaction.amount > 3418260000 { // deny transactions with an amount greater than 1 million USD let _ = signer.deny(transaction); } else { // Approve anything else let _ = signer.approve(transaction); } } }
- Build and Run Your Signer
Now that you've added the Signer library and created the main function, you can build and run your custom signer using Cargo:
cargo build
cargo run
Architecture Overview
In Introduction to sBTC we established that sBTC is a fungible token on the Stacks blockchain, and explained how users interact with the protocol. This chapter takes a closer look at the major entities in the sBTC protocol and briefly explain how they interact. The following chapters goes into more details on each component.
If you are interested in an in-depth technical explanation of the sBTC system in its entirety, be sure to review the proposed SIP.
sBTC builds on the Proof-of-Transfer (PoX) consensus mechanism defined in SIP-007. This SIP introduces the concept of stacking, which is to lock STX for a period of time to earn Bitcoin rewards. Stacking is performed through a special smart contract, called the PoX contract. People who stack are called stackers.
In sBTC we introduce the following changes to Stacks consensus:
- The PoX contract is extended to include sBTC as a SIP-010 fungible token.
- Stacks miners must include sBTC mint and burn transactions in their blocks in response to valid sBTC requests on the Bitcoin chain.
- Stackers must collectively generate a Bitcoin address every reward cycle and publish it in the PoX contract as the sBTC wallet address.
- Stackers are required to respond to sBTC withdrawal requests.
The following chart illustrates the main components of sBTC.
erDiagram "PoX Contract" ||--|| "Stacks blockchain" : "Depoloyed on" "sBTC Token" }o--|| "PoX Contract": "Defined in" "Stackers" }|--|| "PoX Contract": "Participate in" "sBTC Requests" }o--|| "Stacks blockchain": "Validated by" "Stackers" }|--o{ "sBTC Requests": "Respond to" "User" }o--o{ "sBTC Requests": "Create"
Now that we have established the main components of sBTC, we're ready to dig deeper in the actual workings of it. The following three chapters explains different aspects of the sBTC design and can be read in any order.
1 PoX is the abbreviation for the Proof-of-Transfer consensus mechanism introduced in SIP-007.
sBTC Requests and Responses
Requests to the sBTC system happen on the Bitcoin blockchain. In this chapter, we explain the two requests that users can create. We go over all information they must contain, and how the sBTC protocol responds to the requests. For a more in-depth reference on how these requests are represented on the Bitcoin blockchain, see Bitcoin Transactions.
Deposit Request
When a user wishes to deposit BTC in favor of receiving sBTC, they create a deposit request transaction. This is a bitcoin transaction sending the requested deposit amount of BTC to the address provided by the Stackers. In addition, the transaction must also specify to which Stacks address the sBTC should be minted.
The sBTC deposit request transaction will therefore contain the following data:
- Recipient address: The Stacks address which should receive the sBTC.
- sBTC wallet address: The Bitcoin address maintaining custody of the deposited BTC.
- Amount: The amount to deposit.
How the protocol responds to a deposit request
When a deposit request is mined on the Bitcoin blockchain, the next Stacks block must contain an sBTC mint transaction to the recipient address with the designated amount.
This is enforced on a consensus level in Stacks, so that Stacks blocks which do not respond to deposit requests are considered invalid by Miners.
If for some reason the sBTC mint does not materialize, there is a timeout function that will return the user's BTC to them once the subsequent reward cycle finishes.
Withdrawal Request
An sBTC withdraw request is a bitcoin transaction containing data and two outputs. The first output of this transaction marks the recipient address of the BTC to be withdrawn. The second output of this transaction is a small fee subsidy to the stackers intended to cover the transaction cost of fulfilling the withdrawal. Finally, the transaction specifies the amount to be withdrawn and signs the amount and recipient address with the Stacks address from which the sBTC should be burned.
To summarize, the sBTC withdrawal transaction will contain the following data:
- Recipient address: The Bitcoin address which should receive the BTC.
- sBTC wallet address: The Bitcoin address maintaining custody of the deposited BTC.
- Amount: The amount to withdraw.
- Sender address: The Stacks address holding the sBTC to be burned.
How the protocol responds to a withdrawal request
When a withdrawal request is mined on the Bitcoin blockchain, the next Stacks block must contain a sBTC burn transaction burning the requested amount from the sender address. Once the withdrawal request is final1 on the Stacks blockchain, Stackers must fulfill the withdrawal request on the Bitcoin chain by creating a fulfillment transaction.
The fulfillment transaction is a bitcoin transaction sending the requested withdrawal amount to the designated recipient address specified in the Withdrawal request.
Block finality is a property introduced in the Nakamoto release of Stacks, and a requirement for sBTC.
Bitcoin Transactions
This page outlines all transactions which happen on the Bitcoin blockchain within the sBTC protocol, and how they are represented on the bitcoin chain.
Data and opcodes
Common to all sBTC transactions on Bitcoin is that they need to embed data on the Blockchain. To enable this, we require all sBTC transactions to have their first output be a script of the form
OP_RETURN < magic bytes | opcode | data >
where magic bytes
are special bytes required by the Stacks blockchain, the opcode
identifies the type of the sBTC transaction, and the data
field is specific to the transaction type. We use |
as the concatenation operator to denote that all information is pushed as a single byte slice to the bitcoin script.
The magic bytes are
X2
for Stacks mainnetT2
for Stacks testnet
The opcodes for sBTC are
<
: Deposit request>
: Withdrawal request!
: Withdrawal fulfillmentH
: sBTC wallet handoff
The following sections will go throuch each of these transactions and outline what data and outputs they require.
Deposit request
The deposit request contains the following data (incl. opcode and magic byte) in its first output
0 2 3 66
|-------|--|-------------------|
| magic |op| Recipient address |
- Magic:
X2
orT2
- Opcode:
<
- Recipient address: Either a standard or a contract principal encoded as a Clarity value as defined in SIP-005.
The deposit transaction also has a second output which sends the requested amount to the sBTC wallet address.
Withdrawal request
The withdrawal request contains the following data (incl. opcode and magic byte) in its first output
0 2 3 11 76
|------|--|----------|-----------------|
magic op amount signature
- Magic:
X2
orT2
- Opcode:
>
- Amount: The amount to withdraw, as an 8-byte big-endian unsigned integer.
- Signature: A 65 byte recoverable ECDSA signature authenticating the request.
The withdrawal request is required to have two additional outputs beyond the data output. The second output is a dust amount to the recipient address. The third output is a fee subsidy to the sBTC wallet to fund the fulfillment of the withdrawal.
Signature
The recoverable ECDSA signature in the withdrawal request ensures the authenticity and integrity of the transaction. Given that we use the secp256k1 elliptic curve (same as Bitcoin), this signature is composed of 65 bytes.
Message Format:
Before diving into the signature's byte-level structure, let's clarify the format of the message being signed.
The signature attests to a withdrawal request from a specific stacks principal. The signed message encapsulates:
- Amount (8 bytes): The total amount to withdraw, encoded as an 8-byte big-endian unsigned integer.
- Recipient Address (
n
bytes): This represents the intended beneficiary of the withdrawal. It's encoded as a Bitcoin script. The length of this script, denoted byn
, can vary depending on the address format and type.
These components are concatenated in the given order, resulting in a byte array with a total length of n+8
:
0 8 n+8
|------|----------------------|
amount recipient
The actual message that gets signed is not this byte array directly but its SHA-256 hash, producing a consistent 32-byte digest.
Signature Structure:
Now, with the hashed message ready, the structure of the recoverable ECDSA signature is as follows:
0 1 33 65
|---|------|-------|
v r s
-
v (1 byte): The recovery ID, a value between 0 and 3. It's a critical component that facilitates the public key's recovery during verification, eliminating the need for its explicit provision.
-
r (32 bytes): A value linked to the x-coordinate of a point on the elliptic curve. It's a pivotal element in ECDSA's verification mechanism, effectively serving as proof of the signer's private key ownership.
-
s (32 bytes): This, combined with
r
, constitutes the essence of the ECDSA signature. Together, they confirm the validity and authenticity of the message.
For the withdrawal request to be deemed valid, the signature must match the expected data.
Withdrawal fulfillment
The withdrawal request contains the following data (incl. opcode and magic byte) in its first output
0 2 3 35
|------|--|---------------------|
magic op Chain tip
- Magic:
X2
orT2
- Opcode:
!
- Chain tip: The stacks chain tip used to vaildate the withdrawal.
The withdrawal fulfillment has a second output which sends the requested amount to the recipient address. Finally, the withdrawal fulfillment links back to the withdrawal request transaction by consuming the fee subsidy output of the withdrawal request as its first input.
sBTC wallet handoff
The sBTC wallet handoff contains the following data (incl. opcode and magic byte) in its first output
0 2 3 11
|------|--|---------------------|
magic op Reward cycle
- Magic:
X2
orT2
- Opcode:
H
- Reward cycle: The reward cycle number of the Stacker set handing over the sBTC wallet.
The second output sends an amount greater than or equal to the total amount of sBTC in circulation to the sBTC wallet of the next reward cycle.
Commit-Reveal Transactions
Thus far, we have have been introduced to some of the Bitcoin transactions that make it possible to interact with sBTC. However, one big drawback of the transaction formats we have been talking about is that they require the creation of custom Bitcoin transactions, which is a moderately sophisticated use case of Bitcoin and thus remains unsupported by some wallets as well as a lot of custodian solutions that are widely used in the current landscape. Cutting out this portion of the Bitcoin ecosystem from being able to use sBTC is not ideal. As a protocol, it is super important that sBTC is accessible to anyone who wants to use it, and this translates to allowing sBTC compatible transactions to be sent from any viable Bitcoin wallet.
To accommodate for this use case, sBTC also supports an alternate transaction wire-format that has universal compatibility with any Bitcoin wallet. Since this scheme uses two transactions to fulfill a sBTC operation, we call it the commit-reveal scheme. As the name suggests, one of the transactions commits the intent of the user and the second transaction reveals it to the protocol.
Note that commit-reveal does not introduce new sBTC operations, but rather an alternate way to express the same operations . From the perspective of the sBTC protocol, these two schemes are completely compatible and interchangeable with each other, in terms of how they are interpreted by the protocol and the effects they produce.
Let's dig a little deeper into the details.
Operation format
Fundamentally, all sBTC transactions on Bitcoin have to embed some data into the blockchain itself that highlights the intent of the user to interact with the sBTC protocol. The protocol then identifies these intents from the chain and verifies and executes them, thus executing the intent of the user that was previously embedded in the chain.
As long as we have a reliable way to achieve this cycle of embedding an intent into Bitcoin, reading it and processing it, we can fulfill any sBTC operation. Both the direct scheme (SIP-021 style transactions) and transactions that fulfill commit-reveal scheme achieve this, only differing slightly in how the data is embedded into the chain itself.
In the direct scheme, we embed the data directly in the transaction itself, by using an OP_RETURN
output.
In the commit reveal scheme, this embedding is done in two stages: the commit transaction and the reveal transaction.
Commit transaction
The commit transaction is a very simple transaction with only one requirement: it must contain an output to either a p2tr
, p2wsh
, p2sh-p2wsh
or p2sh
address. We need to use these types of addresses because all of them require a user to reveal a script to be spent. We require the revealed script to have the following format:
<DATA> OP_DROP <LOCK SCRIPT...>
where the DATA
part of the script is similar to the corresponding data format of the direct scheme using OP_RETURN
, minus the first two magic bytes (that part will be dealt with in the reveal transaction that follows). The data is at most 86 bytes long (the opcode + payload is at most 78 bytes) and also contains an 8 byte chunk that specifies a fee subsidy, i.e. the amount of funds that the reveal transaction is allowed to spend as transaction fees.
The DATA
section of the script thus looks like this:
0 1 n n + 8
|--|----------------------------|--------------|
op sBTC payload fee subsidy
where the first byte is the opcode of the sBTC transaction, the payload is essential data required for the specific sBTC operation and the fee subsidy limits the maximum amount of money the reveal transaction is allowed to use as fees.
Reveal transaction
The reveal transaction is also fairly simple in construction and MUST satisfy the following:
- It MUST consume an UTXO from a commit transaction as its first input.
- The first output MUST be an
OP_RETURN
output with a three byte payload where the first two bytes are the magic bytes (the same ones we promised to add back) that specify the network they are running on -T2
for mainnet andX2
for testnet, and the last two bytes is an opcode and a script version byte.0 2 3 4 |------|--|---------| magic op version
The opcode identifies which type of script is revealed. It is w
if the script is embedded in segwit witness data, and r
if the script is in a p2sh redeem script.
The version identifies the SegWit witness version. It is 0
for p2wsh
scripts and 1
for p2tr
scripts. If the opcode is r
, this version byte may be omitted.
Because the reveal transaction consumes the UTXO from the commit transaction, the data that was embedded in the script of the commit transaction is revealed. Thus, when the sBTC protocol observes a bitcoin operation with the opcode w
or r
, it indicates a reveal transaction and the data for the intended operation by the initiator of the commit transaction can be found in either the witness or redeem script of the first input.
Any remaining outputs of the reveal transaction must be of the same form as in the direct scheme. For instance, the reveal transaction representing an sBTC withdrawal request must contain two additional outputs (just like its direct scheme counterpart) in order:
- the BTC recipient address
- the funding of the fulfillment transaction.
Processing the commit-reveal scheme at the protocol level
Now that we understand how the low level representations of commit-reveal transactions and what they represent, we need to talk about how the sBTC protocol itself interacts with the scheme to ensure fulfillment of such transactions.
On a high level, this diagram summarizes the interactions between the various parties involved in the fulfillment of the commit-reveal scheme:
sequenceDiagram actor User User ->> sBTC Committer: 1. Provide sBTC operation data sBTC Committer -->> sBTC Revealer: 2. Send witness script and associated data sBTC Committer ->> User: 3. Return commit operation address User ->> Bitcoin: 4. Broadcast commit transaction sBTC Revealer -->> Bitcoin: 5. Read commit transaction sBTC Revealer -->> Bitcoin: 6. Broadcast reveal transaction Stacks -->> Bitcoin: 7. Process reveal transaction as any sBTC operation
More importantly there are three parts of the process that need to interact:
- The Committer (the person the submitting the commit transaction, initiating the scheme)
- The Revealer (the person that consumes commit transactions and initiates reveal transactions)
- The Bitcoin blockchain itself, which provides the underlying data layer which is used to express the scheme
The Committer can be thought of as a system that interacts with a user wallet and constructs commit transactions (like the sBTC bridge).
The Revealer can be thought of a specific system that participates in the sBTC protocol, maintaining a wallet and can consume commit transactions and broadcast reveal transactions. They probably have convenient API endpoints for the Committer to use to construct Witness data.
Here is an example flow of data through the protocol (for illustration purposes only):
- User interacts with a web UI of a Committer, providing operation data to construct the commit transaction
- The Committer constructs a witness script that can be spent by the Revealer
- The Committer then sends this witness script and any other associated data that might be required to construct a reveal transaction to the Revealer
- The Committer returns the address to send the commit operation to the user
- User broadcasts the commit transaction by making a payment to the given commit address
- The Revealer reads the commit transaction, having already processed the witness script
- The Revealer broadcasts the reveal transaction (there can be economic incentives here that makes the Revealer not reveal a commit transaction. For example, if the cost of revealing the transaction is too high, the Revealer might choose to ignore it altogether)
- The sBTC protocol picks up the reveal transaction and fulfills it (by interpreting the operation data from the witness script)
NOTE: It is entirely possible for the Revealer to steal the witness data and use it for its own benefit, although this will be entire visible on the Bitcoin chain data itself and can be completely traced. Thus, there needs to be some degree of cryptoeconomic incentives present that discourage the Revealer from doing this.
sBTC Clarity Contracts
One of the key pieces of the sBTC system is the set of Clarity contracts that facilitate the operations of the sBTC token.
Recall that sBTC is a SIP-010 fungible token on the Stacks chain. This provides an easy-to-use interface for interacting with sBTC.
Upon successful deposit and withdrawal transactions the signer system will call functions in this contract. Here we'll walk through the contracts and explain what each piece is doing so you have a thorough understanding of the Clarity code running sBTC.
:::note It's important to note that sBTC is currently in Developer Release. This is a developer preview so developers can begin learning and experimenting with sBTC before moving to the fully decentralized version. As such, these contracts are subject to change. :::
The Clarity contracts live in the romeo/asset-contract/contracts
folder of the sbtc
repo.
In that folder you'll see three files:
asset.clar
clarity-bitcoin-mini-deploy.clar
clarity-bitcoin-mini.clar
The asset
contract is what does the heavy lifting for sBTC operations. The clarity-bitcoin-mini
library is a stateless utility library to make it easier to interact with Bitcoin. This is a key feature of sBTC and this library provides several helper functions to handle that.
The clarity-bitcoin-mini-deploy
contract is exactly the same, except debug mode is set to false for production.
Now, let's go through the asset
contract.
;; title: wrapped BTC on Stacks
;; version: 0.1.0
;; summary: sBTC dev release asset contract
;; description: sBTC is a wrapped BTC asset on Stacks.
;; It is a fungible token (SIP-10) that is backed 1:1 by BTC
;; For this version the wallet is controlled by a centralized entity.
;; sBTC is minted when BTC is deposited into the wallet and
;; burned when BTC is withdrawn from the wallet.
;; Requests for minting and burning are made by the contract owner.
;; token definitions
;; 100 M sats = 1 sBTC
;; 21 M sBTC supply = 2.1 Q sats total
(define-fungible-token sbtc u2100000000000000)
;; constants
;;
(define-constant err-invalid-caller (err u4))
(define-constant err-forbidden (err u403))
(define-constant err-btc-tx-already-used (err u500))
;; data vars
;;
(define-data-var contract-owner principal tx-sender)
(define-data-var bitcoin-wallet-public-key (optional (buff 33)) none)
;; stores all btc txids that have been used to mint or burn sBTC
(define-map amounts-by-btc-tx (buff 32) int)
;; public functions
;;
;; #[allow(unchecked_data)]
(define-public (set-contract-owner (new-owner principal))
(begin
(try! (is-contract-owner))
(ok (var-set contract-owner new-owner))
)
)
;; #[allow(unchecked_data)]
(define-public (set-bitcoin-wallet-public-key (public-key (buff 33)))
(begin
(try! (is-contract-owner))
(ok (var-set bitcoin-wallet-public-key (some public-key)))
)
)
;; #[allow(unchecked_data)]
(define-public (mint (amount uint)
(destination principal)
(deposit-txid (buff 32))
(burn-chain-height uint)
(merkle-proof (list 14 (buff 32)))
(tx-index uint)
(block-header (buff 80)))
(begin
(try! (is-contract-owner))
(try! (verify-txid-exists-on-burn-chain deposit-txid burn-chain-height merkle-proof tx-index block-header))
(asserts! (map-insert amounts-by-btc-tx deposit-txid (to-int amount)) err-btc-tx-already-used)
(try! (ft-mint? sbtc amount destination))
(print {notification: "mint", payload: deposit-txid})
(ok true)
)
)
;; #[allow(unchecked_data)]
(define-public (burn (amount uint)
(owner principal)
(withdraw-txid (buff 32))
(burn-chain-height uint)
(merkle-proof (list 14 (buff 32)))
(tx-index uint)
(block-header (buff 80)))
(begin
(try! (is-contract-owner))
(try! (verify-txid-exists-on-burn-chain withdraw-txid burn-chain-height merkle-proof tx-index block-header))
(asserts! (map-insert amounts-by-btc-tx withdraw-txid (* -1 (to-int amount))) err-btc-tx-already-used)
(try! (ft-burn? sbtc amount owner))
(print {notification: "burn", payload: withdraw-txid})
(ok true)
)
)
;; #[allow(unchecked_data)]
(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34))))
(begin
(asserts! (or (is-eq tx-sender sender) (is-eq contract-caller sender)) err-invalid-caller)
(try! (ft-transfer? sbtc amount sender recipient))
(match memo to-print (print to-print) 0x)
(ok true)
)
)
;; read only functions
;;
(define-read-only (get-bitcoin-wallet-public-key)
(var-get bitcoin-wallet-public-key)
)
(define-read-only (get-contract-owner)
(var-get contract-owner)
)
(define-read-only (get-name)
(ok "sBTC")
)
(define-read-only (get-symbol)
(ok "sBTC")
)
(define-read-only (get-decimals)
(ok u8)
)
(define-read-only (get-balance (who principal))
(ok (ft-get-balance sbtc who))
)
(define-read-only (get-total-supply)
(ok (ft-get-supply sbtc))
)
(define-read-only (get-token-uri)
(ok (some u"https://gateway.pinata.cloud/ipfs/Qma5P7LFGQAXt7gzkNZGxet5qJcVxgeXsenDXwu9y45hpr?_gl=1*1mxodt*_ga*OTU1OTQzMjE2LjE2OTQwMzk2MjM.*_ga_5RMPXG14TE*MTY5NDA4MzA3OC40LjEuMTY5NDA4MzQzOC42MC4wLjA"))
)
(define-read-only (get-amount-by-btc-txid (btc-txid (buff 32)))
(map-get? amounts-by-btc-tx btc-txid)
)
;; private functions
;;
(define-private (is-contract-owner)
(ok (asserts! (is-eq (var-get contract-owner) contract-caller) err-forbidden))
)
(define-read-only (verify-txid-exists-on-burn-chain (txid (buff 32)) (burn-chain-height uint) (merkle-proof (list 14 (buff 32))) (tx-index uint) (block-header (buff 80)))
(contract-call? .clarity-bitcoin-mini was-txid-mined burn-chain-height txid block-header { tx-index: tx-index, hashes: merkle-proof})
)
Although powerful, the sBTC contract is actually relatively simple. The mint
and burn
functions of the contract will not be interacted with directly by a developer or a user, but instead will be called by the sBTC signer system upon successful deposit and withdrawal requests. The transfer
function, however, will often be called by developers for users looking to transfer their sBTC.
Up at the top we are setting some variables, most notably the sBTC supply, which is set to match 1:1 with BTC supply.
We also have the contract owner and Bitcoin wallet public key which will be set by the sBTC protocol. As per the design documentation, the Bitcoin public key will change every stacking reward cycle to the new threshold wallet.
Next we have two basic public functions to set those two variables.
Finally we are defining a map to define all deposit and withdrawal transaction IDs to ensure that once a particular Bitcoin transaction has been used in a sBTC operation, it can not be used again.
The next three functions comprise the bulk of the sBTC functionality.
Before we get to those, let's take a look at the verify-txid-exists-on-burn-chain
towards the bottom of the contract. This utilizes the helper contract to ensure that a transaction actually happened on the Bitcoin chain.
This native integration with the Bitcoin L1 is one of the great parts of Clarity, as we can verify directly from our smart contracts whether or not a Bitcoin transaction was actually mined, all on chain.
(define-read-only (verify-txid-exists-on-burn-chain (txid (buff 32)) (burn-chain-height uint) (merkle-proof (list 14 (buff 32))) (tx-index uint) (block-header (buff 80)))
(contract-call? .clarity-bitcoin-mini was-txid-mined burn-chain-height txid block-header { tx-index: tx-index, hashes: merkle-proof})
)
This takes in a transaction ID, the Bitcoin block height the transaction was in, and a merkle proof. All of this information is passed to the library to verify that the transaction actually occurred.
For some more context on how this process works and to see how to use it your own projects, be sure to check out the Bitcoin Primer.
Mint
;; #[allow(unchecked_data)]
(define-public (mint (amount uint)
(destination principal)
(deposit-txid (buff 32))
(burn-chain-height uint)
(merkle-proof (list 14 (buff 32)))
(tx-index uint)
(block-header (buff 80)))
(begin
(try! (is-contract-owner))
(try! (verify-txid-exists-on-burn-chain deposit-txid burn-chain-height merkle-proof tx-index block-header))
(asserts! (map-insert amounts-by-btc-tx deposit-txid (to-int amount)) err-btc-tx-already-used)
(try! (ft-mint? sbtc amount destination))
(print {notification: "mint", payload: deposit-txid})
(ok true)
)
)
Now we get to the mint
function. This is the function that is called when a deposit transaction is initiated and processed on the Bitcoin side. The signer will detect that and call the mint
function.
This function takes in some information that is all read directly from the provided Bitcoin transaction. It includes the Stacks principal to mint the sBTC to, and all of the required Bitcoin transaction information required to verify it.
It then checks to make sure the contract owner (the signer system) is calling it, makes sure the Bitcoin transaction actually happened, updates the map of Bitcoin transactions that have been used in sBTC operations, and mints the token to the specified Stacks principal.
Burn
;; #[allow(unchecked_data)]
(define-public (burn (amount uint)
(owner principal)
(withdraw-txid (buff 32))
(burn-chain-height uint)
(merkle-proof (list 14 (buff 32)))
(tx-index uint)
(block-header (buff 80)))
(begin
(try! (is-contract-owner))
(try! (verify-txid-exists-on-burn-chain withdraw-txid burn-chain-height merkle-proof tx-index block-header))
(asserts! (map-insert amounts-by-btc-tx withdraw-txid (* -1 (to-int amount))) err-btc-tx-already-used)
(try! (ft-burn? sbtc amount owner))
(print {notification: "burn", payload: withdraw-txid})
(ok true)
)
)
The burn
function works much the same except it is called upon a successful withdrawal request, when a user wants to convert their sBTC back to BTC.
Transfer
;; #[allow(unchecked_data)]
(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34))))
(begin
(asserts! (or (is-eq tx-sender sender) (is-eq contract-caller sender)) err-invalid-caller)
(try! (ft-transfer? sbtc amount sender recipient))
(match memo to-print (print to-print) 0x)
(ok true)
)
)
Finally we have a basic transfer function that allows users to transfer sBTC between each other.
The rest of the functions are basic read-only
functions that are used to retrieve information about the asset.
Stacker responsibilities
One of the most significant changes to accommodate the sBTC design is that Stackers must now perform active work to continue receiving PoX rewards. Stackers provide partial signatures for BTC withdrawal fulfillment transactions[the BTC wallet] for the duration of each reward cycle in which their STX are locked. This chapter outlines the new role of the stackers, and how they interact with each other to fulfill their duties.
sBTC wallet address generation
Stackers in sBTC operate in Reward cycles similar to previous versions of PoX. However, the reward cycle has been modified to consist of three phases: prepare (80 blocks), handoff (20 blocks), and reward (2000 blocks).
During the prepare phase, miners decide on an anchor block and the next reward cycle's Stackers as before. During the handoff phase, this new set of Stackers MUST collectively determine and provide a single Bitcoin address in the PoX contract to operate as the sBTC wallet address for the next reward cycle. If they fail to do so within the first 10 blocks of the handoff phase, the prepare phase is reinitiated and a new set of stackers will be selected.
If no valid sBTC address is provided, the current set of Stackers' continue operating as before. Their STX will remain frozen, and they will continue to receive PoX rewards until a successful prepare phase and handoff has occurred. However, once the new set of stackers has provided an sBTC wallet address, the current set of Stackers MUST execute a wallet handoff to this newly generated sBTC wallet address.
sBTC Wallet handoff
An sBTC Wallet handoff is used by the current reward cycle's Stackers to send all deposited BTC to the next reward cycle's Stackers' sBTC wallet address within 10 blocks of the reward cycle starting. Additionally, Stackers MUST transfer an equal or greater number of BTC than the amount of sBTC that existed at the end of their own wallet's lifetime. This implies that Stackers MUST cover any fee costs associated with fulfilling sBTC withdrawal requests.
sBTC Withdrawal fulfillment
To fulfill an sBTC withdrawal request, Stackers send one or more Bitcoin transactions that pay the requested amount of BTC to the withdrawal address stipulated by the withdrawal request. If Stackers have received their sBTC wallet handoff and they fail to fulfill a request within 50 Bitcoin blocks of the request being finalized (i.e. at most 150 Bitcoin blocks after the request is submitted), then the system transitions to Recovery mode and PoX payouts are repurposed for fulfilling pending withdrawal requests.
If Stackers do not fulfill the pending sBTC withdrawal requests, their STX will be frozen by .pox and any earned BTC used to fulfill these pending requests. Stackers may only receive back their STX and resume earning BTC once all the withdrawal requests for which they were responsible are fulfilled.
Signature Aggregation with FROST
WSTS Adaptation
Signing protocol
StackerDB
Covers the StackerDB system and how it works.
sBTC Releases
Since sBTC is a complex system, we have defined multiple releases of the system to incrementally add functionality and grow the complexity of the system. These are the planned releases:
Version | Name (optional) | Released |
---|---|---|
0.1 | sBTC Developer | |
1.0 | sBTC Nakamoto | |
1.1 | sBTC Nakamoto v2 |
The following table highlights the main differences between the releases
0.1 | 1.0 | 1.1 | |
---|---|---|---|
sBTC token | ✅ | ✅ | ✅ |
OP_RETURN data | ✅ | ✅ | ✅ |
Commit-Reveal data | ✅ | ✅ | |
Mainnet | ✅ | ✅ | |
Open Membership | ✅ | ✅ | |
Consensus breaking | ✅ | ✅ | |
Liveness ratio | ✅ | ||
Recovery mode | ✅ |
sBTC Developer Release (0.1)
The sBTC Developer Release (sBTC DR) facilitates the complete deposit and withdrawal processes of sBTC, simulating the core mechanics of the sBTC system in the form of a singular service.
This release includes an asset contract alongside a dedicated binary. Primarily designed for use on testnet or local development networks. While it's technically possible for anyone to deploy the sBTC DR on the mainnet, it's not recommended due to its developmental nature.
Upon activation, the sBTC DR binary takes charge: it deploys the asset contract and actively monitors sBTC operations. When a deposit request comes in, the system mints the corresponding sBTC tokens to the specified address. Similarly, upon a withdrawal request, it burns the requisite sBTC tokens and promptly processes the withdrawal, transferring the designated BTC amount to the intended recipient.
graph TD; A[User] --> B[Deposit Request]; A --> C[Withdrawal Request]; D[sBTC DR Binary] --> F[Listen to sBTC Operations]; B --> G[Mint sBTC Tokens]; C --> H[Burn sBTC Tokens]; H --> J[Send BTC to Recipient]; classDef userStyle fill:#f9d457,stroke:#f28482; classDef operationStyle fill:#bee3db,stroke:#2a9d8f; classDef processStyle fill:#f4a261,stroke:#e76f51; class A userStyle; class B,C operationStyle; class D,F,G,H,I,J processStyle; class E operationStyle;
sBTC Developer release reference implementation plan
The reference implementation of the sBTC developer release, codenamed Alpha Romeo, is currently under implementation in this repository.
Every piece of functionality that is being worked on is formulated as issues with the [sBTC DR] prefix and alpha-romeo
label.
For anyone interested in tracking the high-level progress of the Alpha Romeo work, these key issues should provide a good view in how things are progressing.
- Implement the deposit flow #67
- Implement the withdrawal flow #68
- Containerize and deploy the release #85
- Ensure the bridge is compatible with the latest release #86
- Write developer docs for the release #77
The sBTC contract has be deployed on the Stacks testnet at ST1R1061ZT6KPJXQ7PAXPFB6ZAZ6ZWW28G8HXK9G5.asset-3.
In sBTC 0.1, the deposit and withdrawal transactions are processed by a central authority (CA). The flow is as follows:
sequenceDiagram actor AB as Alice-BTC participant CB as CA-BTC participant CS as CA-STX actor AS as Alice-STX CS ->> CS: deploys contract Note over AB, AS: BTC tx before contract deployment are ignored. loop AB ->> CB: sends BTC (deposit) CB ->> CS: processes BTC tx CS ->> AS: mints sBTC end loop AB ->> CB: sends dust (withdrawal) CB ->> CS: processes BTC tx CS ->> AS: burns sBTC CB ->> AB: sends BTC (fullfilment) end
Web app
Bridge on Testnet
A web interface for the sBTC contract on Stacks testnet is available at https://bridge.sbtc.tech/?net=testnet
The web app allows to deposit and withdraw btc from the sbtc wallet. It also provides a list of recent deposit and withdrawal transactions.
Demo application
An open source application written with next.js is availble at https://github.com/kenrogers/lagoon. This application allows to lend sBTC as well as to deposit and withdraw btc. It is used in the end to end tutorial.
Wallet support
These applications use sbtc SDK library that works with Leather wallet version 6.11+.
sBTC cli
See README of the sbtc repo for commands to create and broadcast deposit and withdrawal bitcoin transactions.
sBTC API
There is a public API server for the most common tasks in the context of sBTC. Developers can use this API create deposit and withdrawal request, get information about btc transactions, etc.
Documention is available at bridge.sbtc.tech.
sBTC SDK
stacks.js has added support for sBTC deposit and withdrawal requests. Current work in progress can be seen at stacks.js/feat/add-sbtc-contracts.
The package is published on npmjs.
FAQ
Devenv
What is the meaning of the warning in stacks logs Relayer: Failed to submit Bitcoin transaction
?
It is a warning related to stacks mining and not relevant to sBTC transactions.
How to check progress when starting devenv? When can I start sending deposits?
Wait until the stacks block height (stacks_tip_height
) is 2 or more by checking http://localhost:3999/v2/info.
Initially, the server won't response.
For more detailed information about the progress you can look at ./logs.sh stacks-api
.
Wait until you see a message like Proxy created: / -> http://stacks:20443
or
you can look at ./logs.sh stacks
.
There is a time after Clarity state genesis was computed without log messages. Just keep on waiting...
See the bitcoin blocks syncing and wait for messages like Miner: mined anchored block ...
.
Known Limitation
Protocol
- The sbtc wallet is managed by a central authority, not the set of stackers
- Bitcoin can be deposited to a contract, however, contracts can initiate a withdrawal request. Withdrawal requests can be only submitted via the Bitcoin blockchain.
sBTC 1.0
TODO: #14