Source Code

;; Error codes

;; The `version` part of the recipient address is invalid
(define-constant ERR_INVALID_ADDR_VERSION (err u500))
;; The `hashbytes` part of the recipient address is invalid
(define-constant ERR_INVALID_ADDR_HASHBYTES (err u501))
;; The size of the withdrawal is smaller than the dust limit
(define-constant ERR_DUST_LIMIT (err u502))
;; The request id was invalid / returned 'none'
(define-constant ERR_INVALID_REQUEST (err u503))
;; The caller is not the currently-governing multisig principal
(define-constant ERR_INVALID_CALLER (err u504))
;; The withdrawal request was already processed
(define-constant ERR_ALREADY_PROCESSED (err u505))
;; The paid fee was higher than requested
(define-constant ERR_FEE_TOO_HIGH (err u506))
;; The returned index marks the failed transaction in list
(define-constant ERR_WITHDRAWAL_INDEX_PREFIX (unwrap-err! ERR_WITHDRAWAL_INDEX (err true)))
(define-constant ERR_WITHDRAWAL_INDEX (err u507))
(define-constant ERR_INVALID_BURN_HASH (err u508))

;; Maximum value of an address version as a uint
(define-constant MAX_ADDRESS_VERSION u6)
;; Maximum value of an address version that has a 20-byte hashbytes
;; (0x00, 0x01, 0x02, 0x03, and 0x04 have 20-byte hashbytes)
(define-constant MAX_ADDRESS_VERSION_BUFF_20 u4)
;; Maximum value of an address version that has a 32-byte hashbytes
;; (0x05 and 0x06 have 32-byte hashbytes)
(define-constant MAX_ADDRESS_VERSION_BUFF_32 u6)
;; The minimum amount of sBTC you can withdraw
(define-constant DUST_LIMIT u546)
;; protocol contract type
(define-constant withdraw-role 0x02)

;; Initiate a new withdrawal request.
;;
;; # Notes
;;
;; ## Amounts
;;
;; This function locks up `amount + max-fee` from the tx-sender's account,
;; and when the withdrawal request is accepted, the signers will send
;; `amount` of sats to the recipient and spend an a fee amount to bitcoin
;; miners where fee less than or equal to max-fee. If fee is less than
;; max-fee, then the difference will be minted back to the user when
;; `accept-withdrawal-request` is invoked.
;;
;; ## The recipient
;;
;; This constraints and meaning of the recipient field is summarized as:
;; ```text
;; version == 0x00 and (len hashbytes) == 20 => P2PKH
;; version == 0x01 and (len hashbytes) == 20 => P2SH
;; version == 0x02 and (len hashbytes) == 20 => P2SH-P2WPKH
;; version == 0x03 and (len hashbytes) == 20 => P2SH-P2WSH
;; version == 0x04 and (len hashbytes) == 20 => P2WPKH
;; version == 0x05 and (len hashbytes) == 32 => P2WSH
;; version == 0x06 and (len hashbytes) == 32 => P2TR
;; ```
;; Also see <https://docs.stacks.co/clarity/functions#get-burn-block-info>
;;
;; Below is a detailed breakdown of bitcoin address types and how they map
;; to the clarity value. In what follows below, the network used for the
;; human-readable parts is inherited from the network of the underlying
;; transaction itself (basically, on stacks mainnet we send to mainnet
;; bitcoin addresses and similarly on stacks testnet we send to bitcoin
;; testnet addresses).
;;
;; ### P2PKH
;;
;; Generally speaking, Pay-to-Public-Key-Hash addresses are formed by
;; taking the Hash160 of the public key, prefixing it with one byte (0x00
;; on mainnet and 0x6F on testing) and then base58 encoding the result.
;;
;; To specify this address type as the recipient, the `version` is 0x00 and
;; the `hashbytes` is the Hash160 of the public key.
;;
;;
;; ### P2SH, P2SH-P2WPKH, and P2SH-P2WSH
;;
;; Pay-to-script-hash-* addresses are formed by taking the Hash160 of the
;; locking script, prefixing it with one byte (0x05 on mainnet and 0xC4 on
;; testnet) and base58 encoding the result. The difference between them
;; lies with the locking script. For P2SH-P2WPKH addresses, the locking
;; script is:
;; ```text
;; 0 || <Hash160 of the compressed public key>
;; ```
;; For P2SH-P2WSH addresses, the locking script is:
;; ```text
;; 0 || <sha256 of the redeem script>
;; ```
;; And for P2SH addresses you get to choose the locking script in its
;; entirety.
;;
;; Again, after you construct the locking script you take its Hash160,
;; prefix it with one byte and base58 encode it to form the address. To
;; specify these address types in the recipient, the `version` is 0x01,
;; 0x02, and 0x03 (for P2SH, P2SH-P2WPKH, and P2SH-P2WSH respectively) with
;; the `hashbytes` is the Hash160 of the locking script.
;;
;;
;; ### P2WPKH
;;
;; Pay-to-witness-public-key-hash addresses are formed by creating a
;; witness program made entirely of the Hash160 of the compressed public
;; key.
;;
;; To specify this address type in the recipient, the `version` is 0x04 and
;; the `hashbytes` is the Hash160 of the compressed public key.
;;
;;
;; ### P2WSH
;;
;; Pay-to-witness-script-hash addresses are formed by taking a witness
;; program that is compressed entirely of the SHA256 of the redeem script.
;;
;; To specify this address type in the recipient, the `version` is 0x05 and
;; the `hashbytes` is the SHA256 of the redeem script.
;;
;;
;; ### P2TR
;;
;; Pay-to-taproot addresses are formed by "tweaking" the x-coordinate of a
;; public key with a merkle tree. The result of the tweak is used as the
;; witness program for the address.
;;
;; To specify this address type in the recipient, the `version` is 0x06 and
;; the `hashbytes` is the "tweaked" public key.
(define-public (initiate-withdrawal-request (amount uint)
											(recipient { version: (buff 1), hashbytes: (buff 32) })
											(max-fee uint)
	)
	(begin
		(try! (contract-call? .sbtc-token protocol-lock (+ amount max-fee) tx-sender withdraw-role))
		(asserts! (> amount DUST_LIMIT) ERR_DUST_LIMIT)

		;; Validate the recipient address
		(try! (validate-recipient recipient))

		(ok (try! (contract-call? .sbtc-registry create-withdrawal-request amount max-fee tx-sender recipient burn-block-height)))
	)
)

;; Accept a withdrawal request
(define-public (accept-withdrawal-request (request-id uint)
											(bitcoin-txid (buff 32))
											(signer-bitmap uint)
											(output-index uint)
											(fee uint)
											(burn-hash (buff 32))
											(burn-height uint)
											(sweep-txid (buff 32)))
	(let
		(
			(current-signer-data (contract-call? .sbtc-registry get-current-signer-data))
			(request	(unwrap! (contract-call? .sbtc-registry get-withdrawal-request request-id) ERR_INVALID_REQUEST))
			(requested-max-fee (get max-fee request))
			(requested-amount (get amount request))
			(requester (get sender request))
		)

			;; Verify that Bitcoin hasn't forked by comparing the burn hash provided
			(asserts! (is-eq (some burn-hash) (get-burn-header burn-height)) ERR_INVALID_BURN_HASH)

			;; Check that the caller is the current signer principal
			(asserts! (is-eq (get current-signer-principal current-signer-data) tx-sender) ERR_INVALID_CALLER)

			;; Check whether it was already accepted or rejected
			(asserts! (is-none (get status request)) ERR_ALREADY_PROCESSED)

			;; Check that fee is not higher than requesters max fee
			(asserts! (<= fee requested-max-fee) ERR_FEE_TOO_HIGH)

			;; Burn the locked-sbtc
			(try! (contract-call? .sbtc-token protocol-burn-locked (+ requested-amount requested-max-fee) requester	withdraw-role))

			;; Mint the difference b/w max-fee of the request & fee actually paid back to the user in sBTC
			(if (is-eq (- requested-max-fee fee) u0)
				true
				(try! (contract-call? .sbtc-token protocol-mint (- requested-max-fee fee) requester	withdraw-role))
			)

			;; Call into registry to confirm accepted withdrawal
			(try! (contract-call? .sbtc-registry complete-withdrawal-accept request-id bitcoin-txid output-index signer-bitmap fee burn-hash burn-height sweep-txid))

			(ok true)
	)
)

;; Reject a withdrawal request
(define-public (reject-withdrawal-request (request-id uint) (signer-bitmap uint))
	(let
		 (
			(current-signer-data (contract-call? .sbtc-registry get-current-signer-data))
			(withdrawal (unwrap! (contract-call? .sbtc-registry get-withdrawal-request request-id) ERR_INVALID_REQUEST))
			(requested-max-fee (get max-fee withdrawal))
			(requested-amount (get amount withdrawal))
			(requester (get sender withdrawal))
		 )

		;; Check that the caller is the current signer principal
		(asserts! (is-eq (get current-signer-principal current-signer-data) tx-sender) ERR_INVALID_CALLER)

		;; Check that request status is currently-pending
		(asserts! (is-none (get status withdrawal)) ERR_ALREADY_PROCESSED)

		;; Burn sbtc-locked & re-mint sbtc to original requester
		(try! (contract-call? .sbtc-token protocol-unlock (+ requested-amount requested-max-fee) requester	withdraw-role))

		;; Call into registry to confirm accepted withdrawal
		(try! (contract-call? .sbtc-registry complete-withdrawal-reject request-id signer-bitmap))

		(ok true)
	)
)
;; Complete multiple withdrawal requests
(define-public (complete-withdrawals (withdrawals (list 600
										{request-id: uint,
										status: bool,
										signer-bitmap: uint,
										bitcoin-txid: (optional (buff 32)),
										output-index: (optional uint),
										fee: (optional uint),
										burn-hash: (buff 32),
										burn-height: uint,
										sweep-txid: (optional (buff 32))})))
	(let
			(
					(current-signer-data (contract-call? .sbtc-registry get-current-signer-data))
			)

			;; Check that the caller is the current signer principal
			(asserts! (is-eq (get current-signer-principal current-signer-data) tx-sender) ERR_INVALID_CALLER)

			(fold complete-individual-withdrawal-helper withdrawals (ok u0))
	)
)

(define-private (complete-individual-withdrawal-helper (withdrawal
															{request-id: uint,
															status: bool,
															signer-bitmap: uint,
															bitcoin-txid: (optional (buff 32)),
															output-index: (optional uint),
															fee: (optional uint),
															burn-hash: (buff 32),
															burn-height: uint,
															sweep-txid: (optional (buff 32))})
															(helper-response (response uint uint)))
	(match helper-response
		index
			(let
				(
					(current-request-id (get request-id withdrawal))
					(current-signer-bitmap (get signer-bitmap withdrawal))
					(current-bitcoin-txid (get bitcoin-txid withdrawal))
					(current-output-index (get output-index withdrawal))
					(current-fee (get fee withdrawal))
				)
				(if (get status withdrawal)
					;; accepted
					(begin
						(asserts!
							(and (is-some current-bitcoin-txid) (is-some current-output-index) (is-some current-fee))
							(err (+ ERR_WITHDRAWAL_INDEX_PREFIX (+ u10 index))))
						(unwrap! (accept-withdrawal-request current-request-id (unwrap-panic current-bitcoin-txid) current-signer-bitmap (unwrap-panic current-output-index) (unwrap-panic current-fee) (get burn-hash withdrawal) (get burn-height withdrawal) (unwrap-panic (get sweep-txid withdrawal))) (err (+ ERR_WITHDRAWAL_INDEX_PREFIX (+ u10 index))))
					)
					;; rejected
					(unwrap! (reject-withdrawal-request current-request-id current-signer-bitmap) (err (+ ERR_WITHDRAWAL_INDEX_PREFIX (+ u10 index))))
				)
				(ok (+ index u1))
			)
		err-response
						(err err-response)
	)
)

;; Validation methods

;; Validate that a withdrawal's recipient address is well-formed. The logic
;; here follows the same rules as pox-4.
;;
;; At a high-level, the version must be a uint between 0 and 6 (inclusive),
;; and the length of the hashbytes must be 20 bytes if the version is <= 4,
;; and 32 bytes if the version is 5 or 6.
(define-read-only (validate-recipient (recipient { version: (buff 1), hashbytes: (buff 32) }))
	(let
		(
			(version (get version recipient))
			(hashbytes (get hashbytes recipient))
			(version-int (buff-to-uint-be version))
		)
		;; Validate the `version`
		(asserts! (<= version-int MAX_ADDRESS_VERSION) ERR_INVALID_ADDR_VERSION)
		;; Validate the length of `hashbytes`
		(asserts! (if (<= version-int MAX_ADDRESS_VERSION_BUFF_20)
				;; If version is <= 4, then hashbytes must be 20 bytes
				(is-eq (len hashbytes) u20)
				;; Otherwise, hashbytes must be 32 bytes
				(is-eq (len hashbytes) u32))
			ERR_INVALID_ADDR_HASHBYTES)
		(ok true)
	)
)

;; Return the bitcoin header hash of the bitcoin block at the given height.
(define-read-only (get-burn-header (height uint))
	(get-burn-block-info? header-hash height)
)

Functions (5)

FunctionAccessArgs
accept-withdrawal-requestpublicrequest-id: uint, bitcoin-txid: (buff 32
reject-withdrawal-requestpublicrequest-id: uint, signer-bitmap: uint
complete-withdrawalspublicwithdrawals: (list 600 {request-id: uint, status: bool, signer-bitmap: uint, bitcoin-txid: (optional (buff 32
complete-individual-withdrawal-helperprivatewithdrawal: {request-id: uint, status: bool, signer-bitmap: uint, bitcoin-txid: (optional (buff 32
get-burn-headerread-onlyheight: uint