Skip to content

Commit 92a867f

Browse files
authored
add withdraw signaling (#52)
* add withdrawl signaling * fmt * add withdrawal checks * fmt * cleanup withdraw signaling * cleanup * fmt * cleanup * cleanup * rm untrusted input * cleanup
1 parent 892d01c commit 92a867f

File tree

5 files changed

+179
-120
lines changed

5 files changed

+179
-120
lines changed

README.md

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,27 @@
11
# Sova Contracts
22

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

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

77
## Details
88

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

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

17-
## Build
18-
19-
```shell
20-
# Build the project
21-
forge build
22-
```
13+
- **SovaL1Block** `0x2100000000000000000000000000000000000015`
14+
- **SovaBTC** `0x2100000000000000000000000000000000000020`
15+
- **SovaBitcoin** - Library for Bitcoin precompile interactions.
16+
- **UBTC20** - Base contract extending ERC20 with pending transaction states and Bitcoin finality. Prevents transfers during pending Bitcoin operations and handles deferred accounting for cross-chain finalization.
2317

2418
## Deployed Bytecode verification
2519

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

2822
```shell
29-
# uBTC.sol
30-
forge inspect src/UBTC.sol:UBTC deployedBytecode
23+
# SovaBTC.sol
24+
forge inspect src/SovaBTC.sol:SovaBTC deployedBytecode
3125

3226
# SovaL1Block.sol
3327
forge inspect src/SovaL1Block.sol:SovaL1Block deployedBytecode

src/SovaBTC.sol

Lines changed: 133 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,24 @@ import "./UBTC20.sol";
1717
* @author Sova Labs
1818
*
1919
* Bitcoin meets ERC20. Bitcoin meets composability.
20+
*
21+
* @notice Uses sol compiler 0.8.15 for compatibility with Optimism Bedrock contracts
22+
* that Sova Network used for chain artifacts creation.
23+
* Ref: https://github.com/SovaNetwork/optimism/tree/op-deployer-v0.3.3-mainnet
2024
*/
2125
contract SovaBTC is ISovaBTC, UBTC20, Ownable, ReentrancyGuard {
2226
/// @notice Minimum deposit amount in satoshis
2327
uint64 public minDepositAmount;
2428

25-
/// @notice Maximum deposit amount in satoshis
26-
uint64 public maxDepositAmount;
27-
28-
/// @notice Maximum gas limit amount in satoshis
29-
uint64 public maxGasLimitAmount;
30-
3129
/// @notice Pause state of the contract
3230
bool private _paused;
3331

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

35+
/// @notice Mapping to track authorized withdraw signers
36+
mapping(address => bool) public withdrawSigners;
37+
3738
error InsufficientDeposit();
3839
error InsufficientInput();
3940
error InsufficientAmount();
@@ -53,38 +54,73 @@ contract SovaBTC is ISovaBTC, UBTC20, Ownable, ReentrancyGuard {
5354
error TransactionAlreadyUsed();
5455
error PendingDepositExists();
5556
error PendingWithdrawalExists();
57+
error PendingUserWithdrawalRequestExists();
58+
error UnauthorizedWithdrawSigner();
59+
error SignerAlreadyExists();
60+
error SignerDoesNotExist();
5661

5762
event Deposit(address caller, bytes32 txid, uint256 amount);
58-
event Withdraw(address caller, bytes32 txid, uint256 amount);
63+
event WithdrawSignaled(
64+
address indexed user, uint256 amount, uint64 btcGasBid, uint64 operatorFee, string destination
65+
);
66+
event Withdraw(address user, bytes32 txid, uint256 totalAmount);
5967
event MinDepositAmountUpdated(uint64 oldAmount, uint64 newAmount);
6068
event MaxDepositAmountUpdated(uint64 oldAmount, uint64 newAmount);
6169
event MaxGasLimitAmountUpdated(uint64 oldAmount, uint64 newAmount);
6270
event ContractPausedByOwner(address indexed account);
6371
event ContractUnpausedByOwner(address indexed account);
72+
event WithdrawSignerAdded(address indexed signer);
73+
event WithdrawSignerRemoved(address indexed signer);
74+
75+
constructor() Ownable() {
76+
_initializeOwner(msg.sender);
77+
78+
minDepositAmount = 10_000; // 10,000 sat = 0.0001 BTC
79+
80+
_paused = false;
81+
82+
// Set initial withdrawal signer
83+
withdrawSigners[0xd94FcA65c01b7052469A653dB466cB91d8782125] = true;
84+
emit WithdrawSignerAdded(0xd94FcA65c01b7052469A653dB466cB91d8782125);
85+
}
86+
87+
/// Modifiers
6488

6589
modifier whenNotPaused() {
90+
_whenNotPaused();
91+
_;
92+
}
93+
94+
function _whenNotPaused() internal view {
6695
if (_paused) {
6796
revert ContractPaused();
6897
}
69-
_;
7098
}
7199

72100
modifier whenPaused() {
101+
_whenPaused();
102+
_;
103+
}
104+
105+
function _whenPaused() internal view {
73106
if (!_paused) {
74107
revert ContractNotPaused();
75108
}
76-
_;
77109
}
78110

79-
constructor() Ownable() {
80-
_initializeOwner(msg.sender);
111+
modifier onlyWithdrawSigner() {
112+
_onlyWithdrawSigner();
113+
_;
114+
}
81115

82-
minDepositAmount = 10_000; // (starts at 10,000 sats)
83-
maxDepositAmount = 100_000_000_000; // (starts at 1000 BTC = 100 billion sats)
84-
maxGasLimitAmount = 50_000_000; // (starts at 0.5 BTC = 50,000,000 sats)
85-
_paused = false;
116+
function _onlyWithdrawSigner() internal view {
117+
if (!withdrawSigners[msg.sender]) {
118+
revert UnauthorizedWithdrawSigner();
119+
}
86120
}
87121

122+
/// External
123+
88124
function isPaused() external view returns (bool) {
89125
return _paused;
90126
}
@@ -126,9 +162,6 @@ contract SovaBTC is ISovaBTC, UBTC20, Ownable, ReentrancyGuard {
126162
if (amount < minDepositAmount) {
127163
revert DepositBelowMinimum();
128164
}
129-
if (amount > maxDepositAmount) {
130-
revert DepositAboveMaximum();
131-
}
132165

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

154187
/**
155-
* @notice Withdraws Bitcoin by burning uBTC tokens. Then triggering the signing, and broadcasting
156-
* of a Bitcoin transaction.
188+
* @notice Signals a withdrawal request by saving the withdrawal request.
157189
*
158-
* @dev For obvious reasons the UBTC_SIGN_TX_BYTES precompile is a sensitive endpoint. It is enforced
159-
* in the execution client that only this contract can all that precompile functionality.
160-
* @dev The hope is that in the future more SIGN_TX_BYTES endpoints can be added to the network
161-
* and the backends are controlled by 3rd party signer entities.
190+
* @dev The user must have enough sovaBTC to cover the amount + gas limit + operatorFee fee.
162191
*
163-
* @param amount The amount of satoshis to withdraw
164-
* @param btcGasLimit Specified gas limit for the Bitcoin transaction (in satoshis)
165-
* @param btcBlockHeight The current BTC block height. This is used to source spendable Bitcoin UTXOs
166-
* @param dest The destination Bitcoin address (bech32)
192+
* @param amount The amount of satoshis to withdraw (excluding gas)
193+
* @param btcGasLimit The gas limit bid for the Bitcoin transaction in satoshis
194+
* @param operatorFee The fee offered to the operator for processing the withdrawal in satoshis
195+
* @param dest The Bitcoin address to send the withdrawn BTC to
167196
*/
168-
function withdraw(uint64 amount, uint64 btcGasLimit, uint64 btcBlockHeight, string calldata dest)
197+
function signalWithdraw(uint64 amount, uint64 btcGasLimit, uint64 operatorFee, string calldata dest)
169198
external
170-
nonReentrant
171199
whenNotPaused
172200
{
173-
// Input validation
201+
if (bytes(dest).length == 0) {
202+
revert EmptyDestination();
203+
}
204+
174205
if (amount == 0) {
175206
revert ZeroAmount();
176207
}
@@ -179,26 +210,61 @@ contract SovaBTC is ISovaBTC, UBTC20, Ownable, ReentrancyGuard {
179210
revert ZeroGasLimit();
180211
}
181212

182-
if (btcGasLimit > maxGasLimitAmount) {
183-
revert GasLimitTooHigh();
184-
}
185-
186-
uint256 totalRequired = amount + btcGasLimit;
213+
uint256 totalRequired = amount + btcGasLimit + operatorFee;
187214
if (balanceOf(msg.sender) < totalRequired) {
188215
revert InsufficientAmount();
189216
}
190217

191-
if (_pendingWithdrawals[msg.sender].amount > 0) revert PendingWithdrawalExists();
218+
// check if user already has a pending withdrawal
219+
if (_pendingWithdrawals[msg.sender].amount > 0) {
220+
revert PendingWithdrawalExists();
221+
}
222+
223+
// Store the withdraw request
224+
_pendingUserWithdrawRequests[msg.sender] = UserWithdrawRequest({
225+
amount: amount, btcGasLimit: btcGasLimit, operatorFee: operatorFee, destination: dest
226+
});
227+
228+
emit WithdrawSignaled(msg.sender, amount, btcGasLimit, operatorFee, dest);
229+
}
230+
231+
/**
232+
* @notice Processes a withdrawal signal by broadcasting a signed Bitcoin transaction.
233+
*
234+
* @dev Only authorized withdraw signers can call this function.
235+
*
236+
* @param user The address of the user who signaled the withdrawal
237+
* @param signedTx The signed Bitcoin transaction to broadcast
238+
*/
239+
function withdraw(address user, bytes calldata signedTx, uint64 amount, uint64 btcGasLimit, uint64 operatorFee)
240+
external
241+
whenNotPaused
242+
onlyWithdrawSigner
243+
{
244+
// Check if user has a pending withdrawal already
245+
if (_pendingWithdrawals[user].amount > 0) revert PendingWithdrawalExists();
246+
247+
// decode signed tx so that we know it is a valid bitcoin tx
248+
SovaBitcoin.BitcoinTx memory btcTx = SovaBitcoin.decodeBitcoinTx(signedTx);
249+
250+
uint256 totalAmount = amount + uint256(btcGasLimit) + uint256(operatorFee);
251+
252+
if (balanceOf(user) < totalAmount) revert InsufficientAmount();
192253

193254
// Track pending withdrawal
194-
_setPendingWithdrawal(msg.sender, totalRequired);
255+
_setPendingWithdrawal(user, totalAmount);
195256

196-
// Call Bitcoin precompile to construct the BTC tx and lock the slot
197-
bytes32 btcTxid = SovaBitcoin.vaultSpend(msg.sender, amount, btcGasLimit, btcBlockHeight, dest);
257+
// Clear the withdraw request
258+
delete _pendingUserWithdrawRequests[user];
259+
260+
// Broadcast the signed BTC tx
261+
SovaBitcoin.broadcastBitcoinTx(signedTx);
198262

199-
emit Withdraw(msg.sender, btcTxid, amount);
263+
emit Withdraw(user, btcTx.txid, totalAmount);
200264
}
201265

266+
/// Admin
267+
202268
/**
203269
* @notice Admin function to burn tokens from a specific wallet
204270
*
@@ -215,63 +281,55 @@ contract SovaBTC is ISovaBTC, UBTC20, Ownable, ReentrancyGuard {
215281
* @param _minAmount New minimum deposit amount in satoshis
216282
*/
217283
function setMinDepositAmount(uint64 _minAmount) external onlyOwner {
218-
if (_minAmount >= maxDepositAmount) {
219-
revert InvalidDepositLimits();
220-
}
221-
222284
uint64 oldAmount = minDepositAmount;
223285
minDepositAmount = _minAmount;
224286

225287
emit MinDepositAmountUpdated(oldAmount, _minAmount);
226288
}
227289

228290
/**
229-
* @notice Admin function to set the maximum deposit amount
230-
*
231-
* @param _maxAmount New maximum deposit amount in satoshis
291+
* @notice Admin function to pause the contract
232292
*/
233-
function setMaxDepositAmount(uint64 _maxAmount) external onlyOwner {
234-
if (_maxAmount <= minDepositAmount) {
235-
revert InvalidDepositLimits();
236-
}
237-
238-
uint64 oldAmount = maxDepositAmount;
239-
maxDepositAmount = _maxAmount;
293+
function pause() external onlyOwner whenNotPaused {
294+
_paused = true;
240295

241-
emit MaxDepositAmountUpdated(oldAmount, _maxAmount);
296+
emit ContractPausedByOwner(msg.sender);
242297
}
243298

244299
/**
245-
* @notice Admin function to set the maximum gas limit amount
246-
*
247-
* @param _maxGasLimitAmount New maximum gas limit amount in satoshis
300+
* @notice Admin function to unpause the contract
248301
*/
249-
function setMaxGasLimitAmount(uint64 _maxGasLimitAmount) external onlyOwner {
250-
if (_maxGasLimitAmount == 0) {
251-
revert ZeroAmount();
252-
}
253-
254-
uint64 oldAmount = maxGasLimitAmount;
255-
maxGasLimitAmount = _maxGasLimitAmount;
302+
function unpause() external onlyOwner whenPaused {
303+
_paused = false;
256304

257-
emit MaxGasLimitAmountUpdated(oldAmount, _maxGasLimitAmount);
305+
emit ContractUnpausedByOwner(msg.sender);
258306
}
259307

260308
/**
261-
* @notice Admin function to pause the contract
309+
* @notice Admin function to add a withdraw signer
310+
*
311+
* @param signer The address to add as a withdraw signer
262312
*/
263-
function pause() external onlyOwner whenNotPaused {
264-
_paused = true;
313+
function addWithdrawSigner(address signer) external onlyOwner {
314+
if (withdrawSigners[signer]) {
315+
revert SignerAlreadyExists();
316+
}
265317

266-
emit ContractPausedByOwner(msg.sender);
318+
withdrawSigners[signer] = true;
319+
emit WithdrawSignerAdded(signer);
267320
}
268321

269322
/**
270-
* @notice Admin function to unpause the contract
323+
* @notice Admin function to remove a withdraw signer
324+
*
325+
* @param signer The address to remove as a withdraw signer
271326
*/
272-
function unpause() external onlyOwner whenPaused {
273-
_paused = false;
327+
function removeWithdrawSigner(address signer) external onlyOwner {
328+
if (!withdrawSigners[signer]) {
329+
revert SignerDoesNotExist();
330+
}
274331

275-
emit ContractUnpausedByOwner(msg.sender);
332+
withdrawSigners[signer] = false;
333+
emit WithdrawSignerRemoved(signer);
276334
}
277335
}

0 commit comments

Comments
 (0)