@@ -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 */
2125contract 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