This repository contains the core smart contracts for the ReserveBTC system:
- rBTC-SYNTH — a non-transferable ERC20-like token (soulbound) representing a user's verified BTC reserve (denominated in satoshis).
- VaultWrBTC — a transferable ERC20 wrapper ("wrBTC"). Users can deposit rBTC-SYNTH (moves to escrow) and receive transferable wrBTC; redeeming burns wrBTC and releases rBTC-SYNTH back to free balance.
- rBTCOracle — the coordinator contract that synchronizes each user's confirmed BTC total (in satoshis). It mints/burns rBTC-SYNTH and, if needed, slashes wrBTC in the vault and debits escrow to maintain 1:1 backing with the verified BTC reserve. It also exposes read-only proofs (Merkle binding and totals) for integrators.
forge install # install dependencies
forge build # compile contracts
forge test # run testsEnsure
remappings.txtandfoundry.tomlare configured if adding external libraries.
-
Deploy
rBTCSYNTH(oracleAddress)- Temporarily set
oracleAddress = your EOA(Externally Owned Account) for initial setup.
- Temporarily set
-
Deploy
VaultWrBTC(rBTCSYNTH, rBTCOracle)- Can point to a temporary oracle address initially, then replace with the real oracle.
-
Deploy
rBTCOracle(rBTCSYNTH, VaultWrBTC, MERKLE_ROOT)MERKLE_ROOTcommits to bindings:leaf = keccak(user, keccak(btcAddressBytes)).
-
Link the vault in the token
-
From the oracle address:
rBTCSYNTH.setVault(VaultWrBTC); rBTCSYNTH.freezeVaultAddress(); // optional, makes vault immutable
-
-
Add operators to the oracle
-
Operators are addresses allowed to call
syncVerifiedTotal:rBTCOracle.setOperator(operatorAddress, true);
-
-
Remove temporary oracle authority
- Ensure only the deployed
rBTCOracleremains in control.
- Ensure only the deployed
The off-chain oracle should:
-
Monitor BTC addresses bound to users.
-
Calculate
newTotalSatsfor each user. -
Every ~20 seconds (or custom interval), call:
rBTCOracle.syncVerifiedTotal(user, newTotalSats, round);- If
newTotalSats > currentTotal: mint the difference. - If
newTotalSats < currentTotal: burn from free balance first, then slash wrBTC and debit escrow. roundis an optional counter for tracking updates.
- If
-
Functions:
merkleRoot()— returns the current root.verifyBinding(user, btcAddressBytes, proof)— verifies user/address binding.
-
Leaf format:
keccak(user, keccak(btcAddressBytes)). -
Update via
setMerkleRoot(root)(owner only).
Integrators can query:
-
From
rBTCOracle:verifiedTotalSats(user)— total confirmed reserve.isBacked(user)— ensures rBTC equals confirmed total.verifyBinding(...)— verifies BTC binding.
-
From
rBTCSYNTH:freeBalanceOf(user)— free balance.escrowOf(user)— escrowed balance.totalBackedOf(user)— total balance (free + escrow).
Example adapter:
function _assertBacked(address user, IReserveProofOracle oracle) internal view {
require(oracle.isBacked(user), "Reserve mismatch");
}- Wrap:
rBTCSYNTH.wrap(amount)→ movesamountfrom free to escrow → vault mintswrBTC. - Redeem:
VaultWrBTC.redeem(amount)→ burnswrBTC→ vault callsunwrapFromVaultto return rBTC-SYNTH to free balance.
If reserves drop:
- Oracle slashes wrBTC via
slashFromOracle. - Oracle debits escrow via
debitEscrowFromOracle.
-
Decimals: 8 (satoshis)
-
Roles:
owner— manages operators and Merkle root.operator— allowed to sync totals.
| Contract | Function | Avg Gas |
|---|---|---|
| rBTCSYNTH | wrap | 103985 |
| rBTCSYNTH | redeem (via Vault) | 42087 |
| rBTCSYNTH | mintFromOracle | 43217 |
| VaultWrBTC | redeem | 42087 |
Test cases should include:
- Minting when
newTotalSatsis higher. - Burning when free balance covers.
- Burning + slashing when free balance is insufficient.
- Wrapping and redeeming flows.
- Merkle proof verification.
- Soulbound invariants on rBTC-SYNTH.