diff --git a/contracts/Clarinet.toml b/contracts/Clarinet.toml index cc629dbfc..ce7f9ac27 100644 --- a/contracts/Clarinet.toml +++ b/contracts/Clarinet.toml @@ -15,6 +15,11 @@ path = 'contracts/sbtc-deposit.clar' clarity_version = 3 epoch = 3.0 +[contracts.sbtc-deposit-update-test] +path = 'contracts/sbtc-deposit-update-test.clar' +clarity_version = 3 +epoch = 3.0 + [contracts.sbtc-registry] path = 'contracts/sbtc-registry.clar' clarity_version = 3 diff --git a/contracts/contracts/sbtc-bootstrap-signers.clar b/contracts/contracts/sbtc-bootstrap-signers.clar index b2672bdfa..bbe0d3032 100644 --- a/contracts/contracts/sbtc-bootstrap-signers.clar +++ b/contracts/contracts/sbtc-bootstrap-signers.clar @@ -1,12 +1,9 @@ ;; sBTC Bootstrap Signers contract ;; constants - ;; The required length of public keys (define-constant key-size u33) -;; errors - ;; if err is u200, it's the agg key ;; if err is u210>, it's the key at index (err - 210) (define-constant ERR_KEY_SIZE_PREFIX (unwrap-err! ERR_KEY_SIZE (err true))) @@ -17,14 +14,6 @@ ;; equal to 100% of the total number of signer keys. (define-constant ERR_SIGNATURE_THRESHOLD (err u202)) -;; data vars -;; - -;; data maps -;; - -;; public functions - ;; Rotate keys ;; Used to rotate the keys of the signers. This is called whenever ;; the signer set is updated. @@ -35,15 +24,18 @@ ) (let ( - (current-signer-data (contract-call? .sbtc-registry get-current-signer-data)) (new-signer-principal (pubkeys-to-principal new-keys new-signature-threshold)) ) + + ;; Check that more than 1 key is in the new set + (asserts! (> (len new-keys) u1) ERR_KEY_SIZE) + ;; Check that the signature threshold is valid (asserts! (and (> new-signature-threshold (/ (len new-keys) u2)) (<= new-signature-threshold (len new-keys))) ERR_SIGNATURE_THRESHOLD) - ;; 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 the tx-sender is the current signer principal + (asserts! (is-eq (contract-call? .sbtc-registry get-current-signer-principal) tx-sender) ERR_INVALID_CALLER) ;; Checks that length of each key is exactly 33 bytes (try! (fold signer-key-length-check new-keys (ok u0))) @@ -52,13 +44,22 @@ (asserts! (is-eq (len new-aggregate-pubkey) key-size) ERR_KEY_SIZE) ;; Call into .sbtc-registry to update the keys & address - (ok (try! (contract-call? .sbtc-registry rotate-keys new-keys new-signer-principal new-aggregate-pubkey new-signature-threshold))) + (contract-call? .sbtc-registry rotate-keys new-keys new-signer-principal new-aggregate-pubkey new-signature-threshold) ) ) -;; read only functions +;; Update protocol contract +;; Used to update one of the three protocol contracts +(define-public (update-protocol-contract-wrapper (contract-type (buff 1)) (contract-address principal)) + (begin + ;; Check that the tx-sender is the current signer principal + (asserts! (is-eq (contract-call? .sbtc-registry get-current-signer-principal) tx-sender) ERR_INVALID_CALLER) + ;; Call into .sbtc-registry to update the protocol contract + (contract-call? .sbtc-registry update-protocol-contract contract-type contract-address) + ) +) -;; private functions +;; read only functions ;; Signer Key Length Check ;; Checks that the length of each key is exactly 33 bytes @@ -125,8 +126,6 @@ ) ) - - (define-read-only (bytes-len (bytes (buff 33))) (unwrap-panic (element-at BUFF_TO_BYTE (len bytes))) ) diff --git a/contracts/contracts/sbtc-deposit-update-test.clar b/contracts/contracts/sbtc-deposit-update-test.clar new file mode 100644 index 000000000..8048fd4b9 --- /dev/null +++ b/contracts/contracts/sbtc-deposit-update-test.clar @@ -0,0 +1,107 @@ +;; sBTC Deposit v1 contract (updated) + +;; constants + +;; The required length of a txid +(define-constant txid-length u32) +(define-constant dust-limit u546) + +;; protocol contract type +(define-constant deposit-role 0x01) + +;; error codes +;; TXID used in deposit is not the correct length +(define-constant ERR_TXID_LEN (err u300)) +;; Deposit has already been completed +(define-constant ERR_DEPOSIT_REPLAY (err u301)) +(define-constant ERR_LOWER_THAN_DUST (err u302)) +(define-constant ERR_DEPOSIT_INDEX_PREFIX (unwrap-err! ERR_DEPOSIT (err true))) +(define-constant ERR_DEPOSIT (err u303)) +(define-constant ERR_INVALID_CALLER (err u304)) +(define-constant ERR_INVALID_BURN_HASH (err u305)) + +;; public functions + +;; Accept a new deposit request +;; Note that this function can only be called by the current +;; bootstrap signer set address - it cannot be called by users directly. +;; This function handles the validation & minting of sBTC, it then calls +;; into the sbtc-registry contract to update the state of the protocol +(define-public (complete-deposit-wrapper (txid (buff 32)) + (vout-index uint) + (amount uint) + (recipient principal) + (burn-hash (buff 32)) + (burn-height uint) + (sweep-txid (buff 32))) + (let + ( + (current-signer-data (contract-call? .sbtc-registry get-current-signer-data)) + (replay-fetch (contract-call? .sbtc-registry get-deposit-status txid vout-index)) + ) + + ;; 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 amount is greater than dust limit + (asserts! (>= amount dust-limit) ERR_LOWER_THAN_DUST) + + ;; Check that txid is the correct length + (asserts! (is-eq (len txid) txid-length) ERR_TXID_LEN) + + ;; Check that sweep txid is the correct length + (asserts! (is-eq (len sweep-txid) txid-length) ERR_TXID_LEN) + + ;; Assert that the deposit has not already been completed (no replay) + (asserts! (is-none replay-fetch) ERR_DEPOSIT_REPLAY) + + ;; 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) + + ;; Mint the sBTC to the recipient + (try! (contract-call? .sbtc-token protocol-mint amount recipient deposit-role)) + + ;; Complete the deposit + (contract-call? .sbtc-registry complete-deposit txid vout-index amount recipient burn-hash burn-height sweep-txid) + ) +) + +;; 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) +) + +;; Accept multiple new deposit requests +;; Note that this function can only be called by the current +;; bootstrap signer set address - it cannot be called by users directly. +;; +;; This function handles the validation & minting of sBTC by handling multiple (up to 1000) deposits at a time, +;; it then calls into the sbtc-registry contract to update the state of the protocol. +(define-public (complete-deposits-wrapper + (deposits (list 650 {txid: (buff 32), vout-index: uint, amount: uint, recipient: principal, burn-hash: (buff 32), burn-height: uint, sweep-txid: (buff 32)})) + ) + (begin + ;; Check that the caller is the current signer principal + (asserts! (is-eq + (contract-call? .sbtc-registry get-current-signer-principal) + tx-sender + ) ERR_INVALID_CALLER) + + (fold complete-individual-deposits-helper deposits (ok u0)) + ) +) + +;; private functions +;; #[allow(unchecked_data)] +(define-private (complete-individual-deposits-helper (deposit {txid: (buff 32), vout-index: uint, amount: uint, recipient: principal, burn-hash: (buff 32), burn-height: uint, sweep-txid: (buff 32)}) (helper-response (response uint uint))) + (match helper-response + index + (begin + (unwrap! (complete-deposit-wrapper (get txid deposit) (get vout-index deposit) (get amount deposit) (get recipient deposit) (get burn-hash deposit) (get burn-height deposit) (get sweep-txid deposit)) (err (+ ERR_DEPOSIT_INDEX_PREFIX (+ u10 index)))) + (ok (+ index u1)) + ) + err-response + (err err-response) + ) +) + diff --git a/contracts/contracts/sbtc-deposit.clar b/contracts/contracts/sbtc-deposit.clar index ec0d15b44..12969f920 100644 --- a/contracts/contracts/sbtc-deposit.clar +++ b/contracts/contracts/sbtc-deposit.clar @@ -6,6 +6,9 @@ (define-constant txid-length u32) (define-constant dust-limit u546) +;; protocol contract type +(define-constant deposit-role 0x01) + ;; error codes ;; TXID used in deposit is not the correct length (define-constant ERR_TXID_LEN (err u300)) @@ -17,10 +20,6 @@ (define-constant ERR_INVALID_CALLER (err u304)) (define-constant ERR_INVALID_BURN_HASH (err u305)) -;; data vars - -;; data maps - ;; public functions ;; Accept a new deposit request @@ -60,7 +59,7 @@ (asserts! (is-eq (some burn-hash) (get-burn-header burn-height)) ERR_INVALID_BURN_HASH) ;; Mint the sBTC to the recipient - (try! (contract-call? .sbtc-token protocol-mint amount recipient)) + (try! (contract-call? .sbtc-token protocol-mint amount recipient deposit-role)) ;; Complete the deposit (contract-call? .sbtc-registry complete-deposit txid vout-index amount recipient burn-hash burn-height sweep-txid) diff --git a/contracts/contracts/sbtc-registry.clar b/contracts/contracts/sbtc-registry.clar index c6a986629..5f0e45961 100644 --- a/contracts/contracts/sbtc-registry.clar +++ b/contracts/contracts/sbtc-registry.clar @@ -1,23 +1,40 @@ ;; sBTC Registry contract ;; Error codes - (define-constant ERR_UNAUTHORIZED (err u400)) (define-constant ERR_INVALID_REQUEST_ID (err u401)) (define-constant ERR_AGG_PUBKEY_REPLAY (err u402)) (define-constant ERR_MULTI_SIG_REPLAY (err u403)) +(define-constant ERR_INVALID_PROTOCOL_ID (err u404)) +(define-constant ERR_UNAUTHORIZE_FLAG (err u405)) +(define-constant ERR_UNAUTHORIZED_ROLE (err u406)) -;; Variables +;; Protocol contract type +(define-constant governance-role 0x00) +(define-constant deposit-role 0x01) +(define-constant withdrawal-role 0x02) + +;; Variables (define-data-var last-withdrawal-request-id uint u0) (define-data-var current-signature-threshold uint u0) (define-data-var current-signer-set (list 128 (buff 33)) (list)) (define-data-var current-aggregate-pubkey (buff 33) 0x00) (define-data-var current-signer-principal principal tx-sender) - ;; Maps - +;; Active protocol contracts +(define-map active-protocol-contracts (buff 1) principal) +(map-set active-protocol-contracts governance-role .sbtc-bootstrap-signers) +(map-set active-protocol-contracts deposit-role .sbtc-deposit) +(map-set active-protocol-contracts withdrawal-role .sbtc-withdrawal) +(if is-in-mainnet true (map-set active-protocol-contracts 0x03 tx-sender)) +;; Role for active protocol contracts +(define-map active-protocol-roles principal (buff 1)) +(map-set active-protocol-roles .sbtc-bootstrap-signers governance-role) +(map-set active-protocol-roles .sbtc-deposit deposit-role) +(map-set active-protocol-roles .sbtc-withdrawal withdrawal-role) +(if is-in-mainnet true (map-set active-protocol-roles tx-sender 0x03)) ;; Internal data structure to store withdrawal ;; requests. Requests are associated with a unique ;; request ID. @@ -73,13 +90,6 @@ ;; stored to avoid replay (define-map aggregate-pubkeys (buff 33) bool) -;; Data structure to store the active protocol contracts -(define-map protocol-contracts principal bool) -(map-set protocol-contracts .sbtc-bootstrap-signers true) -(map-set protocol-contracts .sbtc-deposit true) -(map-set protocol-contracts .sbtc-withdrawal true) -(if (not is-in-mainnet) (map-set protocol-contracts tx-sender true) true) - ;; Read-only functions ;; Get a withdrawal request by its ID. ;; This function returns the fields of the withdrawal @@ -138,6 +148,10 @@ (var-get current-signer-set) ) +(define-read-only (get-active-protocol (contract-flag (buff 1))) + (map-get? active-protocol-contracts contract-flag) +) + ;; Public functions @@ -161,7 +175,8 @@ ( (id (increment-last-withdrawal-request-id)) ) - (try! (is-protocol-caller)) + (try! (is-protocol-caller withdrawal-role contract-caller)) + ;; #[allow(unchecked_data)] (map-insert withdrawal-requests id { amount: amount, max-fee: max-fee, @@ -198,7 +213,7 @@ (sweep-txid (buff 32)) ) (begin - (try! (is-protocol-caller)) + (try! (is-protocol-caller withdrawal-role contract-caller)) ;; Mark the withdrawal as completed (map-insert withdrawal-status request-id true) (map-insert completed-withdrawal-sweep request-id { @@ -231,7 +246,7 @@ (signer-bitmap uint) ) (begin - (try! (is-protocol-caller)) + (try! (is-protocol-caller withdrawal-role contract-caller)) ;; Mark the withdrawal as completed (map-insert withdrawal-status request-id false) (print { @@ -260,7 +275,7 @@ (sweep-txid (buff 32)) ) (begin - (try! (is-protocol-caller)) + (try! (is-protocol-caller deposit-role contract-caller)) (map-insert deposit-status {txid: txid, vout-index: vout-index} true) (map-insert completed-deposits {txid: txid, vout-index: vout-index} { amount: amount, @@ -292,7 +307,7 @@ ) (begin ;; Check that caller is protocol contract - (try! (is-protocol-caller)) + (try! (is-protocol-caller governance-role contract-caller)) ;; Check that the aggregate pubkey is not already in the map (asserts! (map-insert aggregate-pubkeys new-aggregate-pubkey true) ERR_AGG_PUBKEY_REPLAY) ;; Update the current signer set @@ -314,8 +329,29 @@ ) ) -;; Private functions +;; Update protocol contract +;; This function can only be called by the active bootstrap-signers contract +(define-public (update-protocol-contract + (contract-type (buff 1)) + (new-contract principal) + ) + (begin + ;; Check that caller is protocol contract + (try! (is-protocol-caller governance-role contract-caller)) + ;; Update the protocol contract + (map-set active-protocol-contracts contract-type new-contract) + ;; Update the protocol role + (map-set active-protocol-roles new-contract contract-type) + (print { + topic: "update-protocol-contract", + contract-type: contract-type, + new-contract: new-contract, + }) + (ok true) + ) +) +;; Private functions ;; Increment the last withdrawal request ID and ;; return the new value. (define-private (increment-last-withdrawal-request-id) @@ -329,11 +365,17 @@ ) ;; Checks whether the contract-caller is a protocol contract -(define-read-only (is-protocol-caller) - (validate-protocol-caller contract-caller) +(define-read-only (is-protocol-caller (contract-flag (buff 1)) (contract principal)) + (validate-protocol-caller contract-flag contract) ) ;; Validate that a given principal is a protocol contract -(define-read-only (validate-protocol-caller (caller principal)) - (ok (asserts! (is-some (map-get? protocol-contracts caller)) ERR_UNAUTHORIZED)) +(define-read-only (validate-protocol-caller (contract-flag (buff 1)) (contract principal)) + (begin + ;; Check that contract-caller is an protocol contract + (asserts! (is-eq (some contract) (map-get? active-protocol-contracts contract-flag)) ERR_UNAUTHORIZED) + ;; Check that flag matches the contract-caller + (asserts! (is-eq (some contract-flag) (map-get? active-protocol-roles contract)) ERR_UNAUTHORIZED) + (ok true) + ) ) diff --git a/contracts/contracts/sbtc-token.clar b/contracts/contracts/sbtc-token.clar index faed1832a..95322966d 100644 --- a/contracts/contracts/sbtc-token.clar +++ b/contracts/contracts/sbtc-token.clar @@ -6,72 +6,72 @@ (define-data-var token-name (string-ascii 32) "sBTC") (define-data-var token-symbol (string-ascii 10) "sBTC") -(define-data-var token-uri (optional (string-utf8 256)) none) +(define-data-var token-uri (optional (string-utf8 256)) (some u"https://ipfs.io/ipfs/bafkreibqnozdui4ntgoh3oo437lvhg7qrsccmbzhgumwwjf2smb3eegyqu")) (define-constant token-decimals u8) ;; --- Protocol functions -(define-public (protocol-transfer (amount uint) (sender principal) (recipient principal)) +(define-public (protocol-transfer (amount uint) (sender principal) (recipient principal) (contract-flag (buff 1))) (begin - (try! (contract-call? .sbtc-registry validate-protocol-caller contract-caller)) + (try! (contract-call? .sbtc-registry validate-protocol-caller contract-flag contract-caller)) (ft-transfer? sbtc-token amount sender recipient) ) ) -(define-public (protocol-lock (amount uint) (owner principal)) +(define-public (protocol-lock (amount uint) (owner principal) (contract-flag (buff 1))) (begin - (try! (contract-call? .sbtc-registry validate-protocol-caller contract-caller)) + (try! (contract-call? .sbtc-registry validate-protocol-caller contract-flag contract-caller)) (try! (ft-burn? sbtc-token amount owner)) (ft-mint? sbtc-token-locked amount owner) ) ) -(define-public (protocol-unlock (amount uint) (owner principal)) +(define-public (protocol-unlock (amount uint) (owner principal) (contract-flag (buff 1))) (begin - (try! (contract-call? .sbtc-registry validate-protocol-caller contract-caller)) + (try! (contract-call? .sbtc-registry validate-protocol-caller contract-flag contract-caller)) (try! (ft-burn? sbtc-token-locked amount owner)) (ft-mint? sbtc-token amount owner) ) ) -(define-public (protocol-mint (amount uint) (recipient principal)) +(define-public (protocol-mint (amount uint) (recipient principal) (contract-flag (buff 1))) (begin - (try! (contract-call? .sbtc-registry validate-protocol-caller contract-caller)) + (try! (contract-call? .sbtc-registry validate-protocol-caller contract-flag contract-caller)) (ft-mint? sbtc-token amount recipient) ) ) -(define-public (protocol-burn (amount uint) (owner principal)) +(define-public (protocol-burn (amount uint) (owner principal) (contract-flag (buff 1))) (begin - (try! (contract-call? .sbtc-registry validate-protocol-caller contract-caller)) + (try! (contract-call? .sbtc-registry validate-protocol-caller contract-flag contract-caller)) (ft-burn? sbtc-token amount owner) ) ) -(define-public (protocol-burn-locked (amount uint) (owner principal)) +(define-public (protocol-burn-locked (amount uint) (owner principal) (contract-flag (buff 1))) (begin - (try! (contract-call? .sbtc-registry validate-protocol-caller contract-caller)) + (try! (contract-call? .sbtc-registry validate-protocol-caller contract-flag contract-caller)) (ft-burn? sbtc-token-locked amount owner) ) ) -(define-public (protocol-set-name (new-name (string-ascii 32))) +(define-public (protocol-set-name (new-name (string-ascii 32)) (contract-flag (buff 1))) (begin - (try! (contract-call? .sbtc-registry validate-protocol-caller contract-caller)) + (try! (contract-call? .sbtc-registry validate-protocol-caller contract-flag contract-caller)) (ok (var-set token-name new-name)) ) ) -(define-public (protocol-set-symbol (new-symbol (string-ascii 10))) +(define-public (protocol-set-symbol (new-symbol (string-ascii 10)) (contract-flag (buff 1))) (begin - (try! (contract-call? .sbtc-registry validate-protocol-caller contract-caller)) + (try! (contract-call? .sbtc-registry validate-protocol-caller contract-flag contract-caller)) (ok (var-set token-symbol new-symbol)) ) ) -(define-public (protocol-set-token-uri (new-uri (optional (string-utf8 256)))) +(define-public (protocol-set-token-uri (new-uri (optional (string-utf8 256))) (contract-flag (buff 1))) (begin - (try! (contract-call? .sbtc-registry validate-protocol-caller contract-caller)) + (try! (contract-call? .sbtc-registry validate-protocol-caller contract-flag contract-caller)) (ok (var-set token-uri new-uri)) ) ) @@ -80,9 +80,9 @@ (ft-mint? sbtc-token (get amount item) (get recipient item)) ) -(define-public (protocol-mint-many (recipients (list 200 {amount: uint, recipient: principal}))) +(define-public (protocol-mint-many (recipients (list 200 {amount: uint, recipient: principal})) (contract-flag (buff 1))) (begin - (try! (contract-call? .sbtc-registry validate-protocol-caller contract-caller)) + (try! (contract-call? .sbtc-registry validate-protocol-caller contract-flag contract-caller)) (ok (map protocol-mint-many-iter recipients)) ) ) diff --git a/contracts/contracts/sbtc-withdrawal.clar b/contracts/contracts/sbtc-withdrawal.clar index e626cc0fa..b36d1b738 100644 --- a/contracts/contracts/sbtc-withdrawal.clar +++ b/contracts/contracts/sbtc-withdrawal.clar @@ -29,6 +29,8 @@ (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. ;; @@ -130,7 +132,7 @@ (max-fee uint) ) (begin - (try! (contract-call? .sbtc-token protocol-lock (+ amount max-fee) tx-sender)) + (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 @@ -171,12 +173,12 @@ (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)) + (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)) + (try! (contract-call? .sbtc-token protocol-mint (- requested-max-fee fee) requester withdraw-role)) ) ;; Call into registry to confirm accepted withdrawal @@ -204,7 +206,7 @@ (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)) + (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)) diff --git a/contracts/deployments/default.simnet-plan.yaml b/contracts/deployments/default.simnet-plan.yaml index 51eeee18c..aa831029e 100644 --- a/contracts/deployments/default.simnet-plan.yaml +++ b/contracts/deployments/default.simnet-plan.yaml @@ -54,6 +54,11 @@ plan: emulated-sender: ST2SBXRBJJTH7GV5J93HJ62W2NRRQ46XYBK92Y039 path: contracts/sbtc-deposit.clar clarity-version: 3 + - emulated-contract-publish: + contract-name: sbtc-deposit-update-test + emulated-sender: ST2SBXRBJJTH7GV5J93HJ62W2NRRQ46XYBK92Y039 + path: contracts/sbtc-deposit-update-test.clar + clarity-version: 3 - emulated-contract-publish: contract-name: sbtc-token-test emulated-sender: ST2SBXRBJJTH7GV5J93HJ62W2NRRQ46XYBK92Y039 diff --git a/contracts/docs/sbtc-bootstrap-signers.md b/contracts/docs/sbtc-bootstrap-signers.md index f99e48c2f..53d59b9f3 100644 --- a/contracts/docs/sbtc-bootstrap-signers.md +++ b/contracts/docs/sbtc-bootstrap-signers.md @@ -7,6 +7,7 @@ sBTC Bootstrap Signers contract **Public functions:** - [`rotate-keys-wrapper`](#rotate-keys-wrapper) +- [`update-protocol-contract-wrapper`](#update-protocol-contract-wrapper) **Read-only functions:** @@ -39,7 +40,7 @@ sBTC Bootstrap Signers contract ### rotate-keys-wrapper -[View in file](../contracts/sbtc-bootstrap-signers.clar#L31) +[View in file](../contracts/sbtc-bootstrap-signers.clar#L20) `(define-public (rotate-keys-wrapper ((new-keys (list 128 (buff 33))) (new-aggregate-pubkey (buff 33)) (new-signature-threshold uint)) (response bool uint))` @@ -58,15 +59,18 @@ the signer set is updated. ) (let ( - (current-signer-data (contract-call? .sbtc-registry get-current-signer-data)) (new-signer-principal (pubkeys-to-principal new-keys new-signature-threshold)) ) + + ;; Check that more than 1 key is in the new set + (asserts! (> (len new-keys) u1) ERR_KEY_SIZE) + ;; Check that the signature threshold is valid (asserts! (and (> new-signature-threshold (/ (len new-keys) u2)) (<= new-signature-threshold (len new-keys))) ERR_SIGNATURE_THRESHOLD) - ;; 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 the tx-sender is the current signer principal + (asserts! (is-eq (contract-call? .sbtc-registry get-current-signer-principal) tx-sender) ERR_INVALID_CALLER) ;; Checks that length of each key is exactly 33 bytes (try! (fold signer-key-length-check new-keys (ok u0))) @@ -75,7 +79,7 @@ the signer set is updated. (asserts! (is-eq (len new-aggregate-pubkey) key-size) ERR_KEY_SIZE) ;; Call into .sbtc-registry to update the keys & address - (ok (try! (contract-call? .sbtc-registry rotate-keys new-keys new-signer-principal new-aggregate-pubkey new-signature-threshold))) + (contract-call? .sbtc-registry rotate-keys new-keys new-signer-principal new-aggregate-pubkey new-signature-threshold) ) ) ``` @@ -90,9 +94,41 @@ the signer set is updated. | new-aggregate-pubkey | (buff 33) | | new-signature-threshold | uint | +### update-protocol-contract-wrapper + +[View in file](../contracts/sbtc-bootstrap-signers.clar#L53) + +`(define-public (update-protocol-contract-wrapper ((contract-type (buff 1)) (contract-address principal)) (response bool uint))` + +Update protocol contract +Used to update one of the three protocol contracts + +
+ Source code: + +```clarity +(define-public (update-protocol-contract-wrapper (contract-type (buff 1)) (contract-address principal)) + (begin + ;; Check that the tx-sender is the current signer principal + (asserts! (is-eq (contract-call? .sbtc-registry get-current-signer-principal) tx-sender) ERR_INVALID_CALLER) + ;; Call into .sbtc-registry to update the protocol contract + (contract-call? .sbtc-registry update-protocol-contract contract-type contract-address) + ) +) +``` + +
+ +**Parameters:** + +| Name | Type | +| ---------------- | --------- | +| contract-type | (buff 1) | +| contract-address | principal | + ### signer-key-length-check -[View in file](../contracts/sbtc-bootstrap-signers.clar#L65) +[View in file](../contracts/sbtc-bootstrap-signers.clar#L66) `(define-private (signer-key-length-check ((current-key (buff 33)) (helper-response (response uint uint))) (response uint uint))` @@ -127,7 +163,7 @@ Checks that the length of each key is exactly 33 bytes ### pubkeys-to-spend-script -[View in file](../contracts/sbtc-bootstrap-signers.clar#L80) +[View in file](../contracts/sbtc-bootstrap-signers.clar#L81) `(define-read-only (pubkeys-to-spend-script ((pubkeys (list 128 (buff 33))) (m uint)) (buff 4355))` @@ -160,7 +196,7 @@ Generate the p2sh redeem script for a multisig ### pubkeys-to-hash -[View in file](../contracts/sbtc-bootstrap-signers.clar#L92) +[View in file](../contracts/sbtc-bootstrap-signers.clar#L93) `(define-read-only (pubkeys-to-hash ((pubkeys (list 128 (buff 33))) (m uint)) (buff 20))` @@ -189,7 +225,7 @@ hash160 of the p2sh redeem script ### pubkeys-to-principal -[View in file](../contracts/sbtc-bootstrap-signers.clar#L100) +[View in file](../contracts/sbtc-bootstrap-signers.clar#L101) `(define-read-only (pubkeys-to-principal ((pubkeys (list 128 (buff 33))) (m uint)) principal)` @@ -221,7 +257,7 @@ Given a set of pubkeys and an m-of-n, generate a principal ### pubkeys-to-bytes -[View in file](../contracts/sbtc-bootstrap-signers.clar#L111) +[View in file](../contracts/sbtc-bootstrap-signers.clar#L112) `(define-read-only (pubkeys-to-bytes ((pubkeys (list 128 (buff 33)))) (buff 4352))` @@ -246,7 +282,7 @@ Concat a list of pubkeys into a buffer with length prefixes ### concat-pubkeys-fold -[View in file](../contracts/sbtc-bootstrap-signers.clar#L118) +[View in file](../contracts/sbtc-bootstrap-signers.clar#L119) `(define-read-only (concat-pubkeys-fold ((pubkey (buff 33)) (iterator (buff 4352))) (buff 4352))` @@ -280,7 +316,7 @@ for the public keys and 128 bytes for the length prefixes. ### bytes-len -[View in file](../contracts/sbtc-bootstrap-signers.clar#L130) +[View in file](../contracts/sbtc-bootstrap-signers.clar#L129) `(define-read-only (bytes-len ((bytes (buff 33))) (buff 1))` @@ -303,7 +339,7 @@ for the public keys and 128 bytes for the length prefixes. ### uint-to-byte -[View in file](../contracts/sbtc-bootstrap-signers.clar#L134) +[View in file](../contracts/sbtc-bootstrap-signers.clar#L133) `(define-read-only (uint-to-byte ((n uint)) (buff 1))` @@ -332,13 +368,14 @@ for the public keys and 128 bytes for the length prefixes. ### key-size +constants The required length of public keys ```clarity (define-constant key-size u33) ``` -[View in file](../contracts/sbtc-bootstrap-signers.clar#L6) +[View in file](../contracts/sbtc-bootstrap-signers.clar#L5) ### ERR_KEY_SIZE_PREFIX @@ -349,7 +386,7 @@ if err is u210>, it's the key at index (err - 210) (define-constant ERR_KEY_SIZE_PREFIX (unwrap-err! ERR_KEY_SIZE (err true))) ``` -[View in file](../contracts/sbtc-bootstrap-signers.clar#L12) +[View in file](../contracts/sbtc-bootstrap-signers.clar#L9) ### ERR_KEY_SIZE @@ -357,7 +394,7 @@ if err is u210>, it's the key at index (err - 210) (define-constant ERR_KEY_SIZE (err u200)) ``` -[View in file](../contracts/sbtc-bootstrap-signers.clar#L13) +[View in file](../contracts/sbtc-bootstrap-signers.clar#L10) ### ERR_INVALID_CALLER @@ -367,7 +404,7 @@ The function caller is not the current signer principal (define-constant ERR_INVALID_CALLER (err u201)) ``` -[View in file](../contracts/sbtc-bootstrap-signers.clar#L15) +[View in file](../contracts/sbtc-bootstrap-signers.clar#L12) ### ERR_SIGNATURE_THRESHOLD @@ -378,7 +415,7 @@ equal to 100% of the total number of signer keys. (define-constant ERR_SIGNATURE_THRESHOLD (err u202)) ``` -[View in file](../contracts/sbtc-bootstrap-signers.clar#L18) +[View in file](../contracts/sbtc-bootstrap-signers.clar#L15) ### BUFF_TO_BYTE @@ -403,4 +440,4 @@ equal to 100% of the total number of signer keys. )) ``` -[View in file](../contracts/sbtc-bootstrap-signers.clar#L138) +[View in file](../contracts/sbtc-bootstrap-signers.clar#L137) diff --git a/contracts/tests/clarigen-types.ts b/contracts/tests/clarigen-types.ts index b05b59c95..e611ced99 100644 --- a/contracts/tests/clarigen-types.ts +++ b/contracts/tests/clarigen-types.ts @@ -53,6 +53,21 @@ export const contracts = { ], Response >, + updateProtocolContractWrapper: { + name: "update-protocol-contract-wrapper", + access: "public", + args: [ + { name: "contract-type", type: { buffer: { length: 1 } } }, + { name: "contract-address", type: "principal" }, + ], + outputs: { type: { response: { ok: "bool", error: "uint128" } } }, + } as TypedAbiFunction< + [ + contractType: TypedAbiArg, + contractAddress: TypedAbiArg, + ], + Response + >, bytesLen: { name: "bytes-len", access: "read_only", @@ -680,6 +695,15 @@ export const contracts = { }, access: "constant", } as TypedAbiVariable>, + depositRole: { + name: "deposit-role", + type: { + buffer: { + length: 1, + }, + }, + access: "constant", + } as TypedAbiVariable, dustLimit: { name: "dust-limit", type: "uint128", @@ -717,6 +741,7 @@ export const contracts = { isOk: false, value: 300n, }, + depositRole: Uint8Array.from([1]), dustLimit: 546n, txidLength: 32n, }, @@ -726,6 +751,253 @@ export const contracts = { clarity_version: "Clarity3", contractName: "sbtc-deposit", }, + sbtcDepositUpdateTest: { + functions: { + completeIndividualDepositsHelper: { + name: "complete-individual-deposits-helper", + access: "private", + args: [ + { + name: "deposit", + type: { + tuple: [ + { name: "amount", type: "uint128" }, + { name: "burn-hash", type: { buffer: { length: 32 } } }, + { name: "burn-height", type: "uint128" }, + { name: "recipient", type: "principal" }, + { name: "sweep-txid", type: { buffer: { length: 32 } } }, + { name: "txid", type: { buffer: { length: 32 } } }, + { name: "vout-index", type: "uint128" }, + ], + }, + }, + { + name: "helper-response", + type: { response: { ok: "uint128", error: "uint128" } }, + }, + ], + outputs: { type: { response: { ok: "uint128", error: "uint128" } } }, + } as TypedAbiFunction< + [ + deposit: TypedAbiArg< + { + amount: number | bigint; + burnHash: Uint8Array; + burnHeight: number | bigint; + recipient: string; + sweepTxid: Uint8Array; + txid: Uint8Array; + voutIndex: number | bigint; + }, + "deposit" + >, + helperResponse: TypedAbiArg< + Response, + "helperResponse" + >, + ], + Response + >, + completeDepositWrapper: { + name: "complete-deposit-wrapper", + access: "public", + args: [ + { name: "txid", type: { buffer: { length: 32 } } }, + { name: "vout-index", type: "uint128" }, + { name: "amount", type: "uint128" }, + { name: "recipient", type: "principal" }, + { name: "burn-hash", type: { buffer: { length: 32 } } }, + { name: "burn-height", type: "uint128" }, + { name: "sweep-txid", type: { buffer: { length: 32 } } }, + ], + outputs: { type: { response: { ok: "bool", error: "uint128" } } }, + } as TypedAbiFunction< + [ + txid: TypedAbiArg, + voutIndex: TypedAbiArg, + amount: TypedAbiArg, + recipient: TypedAbiArg, + burnHash: TypedAbiArg, + burnHeight: TypedAbiArg, + sweepTxid: TypedAbiArg, + ], + Response + >, + completeDepositsWrapper: { + name: "complete-deposits-wrapper", + access: "public", + args: [ + { + name: "deposits", + type: { + list: { + type: { + tuple: [ + { name: "amount", type: "uint128" }, + { name: "burn-hash", type: { buffer: { length: 32 } } }, + { name: "burn-height", type: "uint128" }, + { name: "recipient", type: "principal" }, + { name: "sweep-txid", type: { buffer: { length: 32 } } }, + { name: "txid", type: { buffer: { length: 32 } } }, + { name: "vout-index", type: "uint128" }, + ], + }, + length: 650, + }, + }, + }, + ], + outputs: { type: { response: { ok: "uint128", error: "uint128" } } }, + } as TypedAbiFunction< + [ + deposits: TypedAbiArg< + { + amount: number | bigint; + burnHash: Uint8Array; + burnHeight: number | bigint; + recipient: string; + sweepTxid: Uint8Array; + txid: Uint8Array; + voutIndex: number | bigint; + }[], + "deposits" + >, + ], + Response + >, + getBurnHeader: { + name: "get-burn-header", + access: "read_only", + args: [{ name: "height", type: "uint128" }], + outputs: { type: { optional: { buffer: { length: 32 } } } }, + } as TypedAbiFunction< + [height: TypedAbiArg], + Uint8Array | null + >, + }, + maps: {}, + variables: { + ERR_DEPOSIT: { + name: "ERR_DEPOSIT", + type: { + response: { + ok: "none", + error: "uint128", + }, + }, + access: "constant", + } as TypedAbiVariable>, + ERR_DEPOSIT_INDEX_PREFIX: { + name: "ERR_DEPOSIT_INDEX_PREFIX", + type: "uint128", + access: "constant", + } as TypedAbiVariable, + ERR_DEPOSIT_REPLAY: { + name: "ERR_DEPOSIT_REPLAY", + type: { + response: { + ok: "none", + error: "uint128", + }, + }, + access: "constant", + } as TypedAbiVariable>, + ERR_INVALID_BURN_HASH: { + name: "ERR_INVALID_BURN_HASH", + type: { + response: { + ok: "none", + error: "uint128", + }, + }, + access: "constant", + } as TypedAbiVariable>, + ERR_INVALID_CALLER: { + name: "ERR_INVALID_CALLER", + type: { + response: { + ok: "none", + error: "uint128", + }, + }, + access: "constant", + } as TypedAbiVariable>, + ERR_LOWER_THAN_DUST: { + name: "ERR_LOWER_THAN_DUST", + type: { + response: { + ok: "none", + error: "uint128", + }, + }, + access: "constant", + } as TypedAbiVariable>, + ERR_TXID_LEN: { + name: "ERR_TXID_LEN", + type: { + response: { + ok: "none", + error: "uint128", + }, + }, + access: "constant", + } as TypedAbiVariable>, + depositRole: { + name: "deposit-role", + type: { + buffer: { + length: 1, + }, + }, + access: "constant", + } as TypedAbiVariable, + dustLimit: { + name: "dust-limit", + type: "uint128", + access: "constant", + } as TypedAbiVariable, + txidLength: { + name: "txid-length", + type: "uint128", + access: "constant", + } as TypedAbiVariable, + }, + constants: { + ERR_DEPOSIT: { + isOk: false, + value: 303n, + }, + ERR_DEPOSIT_INDEX_PREFIX: 303n, + ERR_DEPOSIT_REPLAY: { + isOk: false, + value: 301n, + }, + ERR_INVALID_BURN_HASH: { + isOk: false, + value: 305n, + }, + ERR_INVALID_CALLER: { + isOk: false, + value: 304n, + }, + ERR_LOWER_THAN_DUST: { + isOk: false, + value: 302n, + }, + ERR_TXID_LEN: { + isOk: false, + value: 300n, + }, + depositRole: Uint8Array.from([1]), + dustLimit: 546n, + txidLength: 32n, + }, + non_fungible_tokens: [], + fungible_tokens: [], + epoch: "Epoch30", + clarity_version: "Clarity3", + contractName: "sbtc-deposit-update-test", + }, sbtcRegistry: { functions: { incrementLastWithdrawalRequestId: { @@ -861,6 +1133,30 @@ export const contracts = { ], Response >, + updateProtocolContract: { + name: "update-protocol-contract", + access: "public", + args: [ + { name: "contract-type", type: { buffer: { length: 1 } } }, + { name: "new-contract", type: "principal" }, + ], + outputs: { type: { response: { ok: "bool", error: "uint128" } } }, + } as TypedAbiFunction< + [ + contractType: TypedAbiArg, + newContract: TypedAbiArg, + ], + Response + >, + getActiveProtocol: { + name: "get-active-protocol", + access: "read_only", + args: [{ name: "contract-flag", type: { buffer: { length: 1 } } }], + outputs: { type: { optional: "principal" } }, + } as TypedAbiFunction< + [contractFlag: TypedAbiArg], + string | null + >, getCompletedDeposit: { name: "get-completed-deposit", access: "read_only", @@ -1026,20 +1322,45 @@ export const contracts = { isProtocolCaller: { name: "is-protocol-caller", access: "read_only", - args: [], + args: [ + { name: "contract-flag", type: { buffer: { length: 1 } } }, + { name: "contract", type: "principal" }, + ], outputs: { type: { response: { ok: "bool", error: "uint128" } } }, - } as TypedAbiFunction<[], Response>, + } as TypedAbiFunction< + [ + contractFlag: TypedAbiArg, + contract: TypedAbiArg, + ], + Response + >, validateProtocolCaller: { name: "validate-protocol-caller", access: "read_only", - args: [{ name: "caller", type: "principal" }], + args: [ + { name: "contract-flag", type: { buffer: { length: 1 } } }, + { name: "contract", type: "principal" }, + ], outputs: { type: { response: { ok: "bool", error: "uint128" } } }, } as TypedAbiFunction< - [caller: TypedAbiArg], + [ + contractFlag: TypedAbiArg, + contract: TypedAbiArg, + ], Response >, }, maps: { + activeProtocolContracts: { + name: "active-protocol-contracts", + key: { buffer: { length: 1 } }, + value: "principal", + } as TypedAbiMap, + activeProtocolRoles: { + name: "active-protocol-roles", + key: "principal", + value: { buffer: { length: 1 } }, + } as TypedAbiMap, aggregatePubkeys: { name: "aggregate-pubkeys", key: { buffer: { length: 33 } }, @@ -1109,11 +1430,6 @@ export const contracts = { }, boolean >, - protocolContracts: { - name: "protocol-contracts", - key: "principal", - value: "bool", - } as TypedAbiMap, withdrawalRequests: { name: "withdrawal-requests", key: "uint128", @@ -1164,6 +1480,16 @@ export const contracts = { }, access: "constant", } as TypedAbiVariable>, + ERR_INVALID_PROTOCOL_ID: { + name: "ERR_INVALID_PROTOCOL_ID", + type: { + response: { + ok: "none", + error: "uint128", + }, + }, + access: "constant", + } as TypedAbiVariable>, ERR_INVALID_REQUEST_ID: { name: "ERR_INVALID_REQUEST_ID", type: { @@ -1194,6 +1520,53 @@ export const contracts = { }, access: "constant", } as TypedAbiVariable>, + ERR_UNAUTHORIZED_ROLE: { + name: "ERR_UNAUTHORIZED_ROLE", + type: { + response: { + ok: "none", + error: "uint128", + }, + }, + access: "constant", + } as TypedAbiVariable>, + ERR_UNAUTHORIZE_FLAG: { + name: "ERR_UNAUTHORIZE_FLAG", + type: { + response: { + ok: "none", + error: "uint128", + }, + }, + access: "constant", + } as TypedAbiVariable>, + depositRole: { + name: "deposit-role", + type: { + buffer: { + length: 1, + }, + }, + access: "constant", + } as TypedAbiVariable, + governanceRole: { + name: "governance-role", + type: { + buffer: { + length: 1, + }, + }, + access: "constant", + } as TypedAbiVariable, + withdrawalRole: { + name: "withdrawal-role", + type: { + buffer: { + length: 1, + }, + }, + access: "constant", + } as TypedAbiVariable, currentAggregatePubkey: { name: "current-aggregate-pubkey", type: { @@ -1238,6 +1611,10 @@ export const contracts = { isOk: false, value: 402n, }, + ERR_INVALID_PROTOCOL_ID: { + isOk: false, + value: 404n, + }, ERR_INVALID_REQUEST_ID: { isOk: false, value: 401n, @@ -1250,11 +1627,22 @@ export const contracts = { isOk: false, value: 400n, }, + ERR_UNAUTHORIZED_ROLE: { + isOk: false, + value: 406n, + }, + ERR_UNAUTHORIZE_FLAG: { + isOk: false, + value: 405n, + }, currentAggregatePubkey: Uint8Array.from([0]), currentSignatureThreshold: 0n, currentSignerPrincipal: "ST2SBXRBJJTH7GV5J93HJ62W2NRRQ46XYBK92Y039", currentSignerSet: [], + depositRole: Uint8Array.from([1]), + governanceRole: Uint8Array.from([0]), lastWithdrawalRequestId: 0n, + withdrawalRole: Uint8Array.from([2]), }, non_fungible_tokens: [], fungible_tokens: [], @@ -1339,12 +1727,14 @@ export const contracts = { args: [ { name: "amount", type: "uint128" }, { name: "owner", type: "principal" }, + { name: "contract-flag", type: { buffer: { length: 1 } } }, ], outputs: { type: { response: { ok: "bool", error: "uint128" } } }, } as TypedAbiFunction< [ amount: TypedAbiArg, owner: TypedAbiArg, + contractFlag: TypedAbiArg, ], Response >, @@ -1354,12 +1744,14 @@ export const contracts = { args: [ { name: "amount", type: "uint128" }, { name: "owner", type: "principal" }, + { name: "contract-flag", type: { buffer: { length: 1 } } }, ], outputs: { type: { response: { ok: "bool", error: "uint128" } } }, } as TypedAbiFunction< [ amount: TypedAbiArg, owner: TypedAbiArg, + contractFlag: TypedAbiArg, ], Response >, @@ -1369,12 +1761,14 @@ export const contracts = { args: [ { name: "amount", type: "uint128" }, { name: "owner", type: "principal" }, + { name: "contract-flag", type: { buffer: { length: 1 } } }, ], outputs: { type: { response: { ok: "bool", error: "uint128" } } }, } as TypedAbiFunction< [ amount: TypedAbiArg, owner: TypedAbiArg, + contractFlag: TypedAbiArg, ], Response >, @@ -1384,12 +1778,14 @@ export const contracts = { args: [ { name: "amount", type: "uint128" }, { name: "recipient", type: "principal" }, + { name: "contract-flag", type: { buffer: { length: 1 } } }, ], outputs: { type: { response: { ok: "bool", error: "uint128" } } }, } as TypedAbiFunction< [ amount: TypedAbiArg, recipient: TypedAbiArg, + contractFlag: TypedAbiArg, ], Response >, @@ -1411,6 +1807,7 @@ export const contracts = { }, }, }, + { name: "contract-flag", type: { buffer: { length: 1 } } }, ], outputs: { type: { @@ -1434,16 +1831,23 @@ export const contracts = { }[], "recipients" >, + contractFlag: TypedAbiArg, ], Response[], bigint> >, protocolSetName: { name: "protocol-set-name", access: "public", - args: [{ name: "new-name", type: { "string-ascii": { length: 32 } } }], + args: [ + { name: "new-name", type: { "string-ascii": { length: 32 } } }, + { name: "contract-flag", type: { buffer: { length: 1 } } }, + ], outputs: { type: { response: { ok: "bool", error: "uint128" } } }, } as TypedAbiFunction< - [newName: TypedAbiArg], + [ + newName: TypedAbiArg, + contractFlag: TypedAbiArg, + ], Response >, protocolSetSymbol: { @@ -1451,10 +1855,14 @@ export const contracts = { access: "public", args: [ { name: "new-symbol", type: { "string-ascii": { length: 10 } } }, + { name: "contract-flag", type: { buffer: { length: 1 } } }, ], outputs: { type: { response: { ok: "bool", error: "uint128" } } }, } as TypedAbiFunction< - [newSymbol: TypedAbiArg], + [ + newSymbol: TypedAbiArg, + contractFlag: TypedAbiArg, + ], Response >, protocolSetTokenUri: { @@ -1465,10 +1873,14 @@ export const contracts = { name: "new-uri", type: { optional: { "string-utf8": { length: 256 } } }, }, + { name: "contract-flag", type: { buffer: { length: 1 } } }, ], outputs: { type: { response: { ok: "bool", error: "uint128" } } }, } as TypedAbiFunction< - [newUri: TypedAbiArg], + [ + newUri: TypedAbiArg, + contractFlag: TypedAbiArg, + ], Response >, protocolTransfer: { @@ -1478,6 +1890,7 @@ export const contracts = { { name: "amount", type: "uint128" }, { name: "sender", type: "principal" }, { name: "recipient", type: "principal" }, + { name: "contract-flag", type: { buffer: { length: 1 } } }, ], outputs: { type: { response: { ok: "bool", error: "uint128" } } }, } as TypedAbiFunction< @@ -1485,6 +1898,7 @@ export const contracts = { amount: TypedAbiArg, sender: TypedAbiArg, recipient: TypedAbiArg, + contractFlag: TypedAbiArg, ], Response >, @@ -1494,12 +1908,14 @@ export const contracts = { args: [ { name: "amount", type: "uint128" }, { name: "owner", type: "principal" }, + { name: "contract-flag", type: { buffer: { length: 1 } } }, ], outputs: { type: { response: { ok: "bool", error: "uint128" } } }, } as TypedAbiFunction< [ amount: TypedAbiArg, owner: TypedAbiArg, + contractFlag: TypedAbiArg, ], Response >, @@ -1695,7 +2111,8 @@ export const contracts = { tokenDecimals: 8n, tokenName: "sBTC", tokenSymbol: "sBTC", - tokenUri: null, + tokenUri: + "https://ipfs.io/ipfs/bafkreibqnozdui4ntgoh3oo437lvhg7qrsccmbzhgumwwjf2smb3eegyqu", }, non_fungible_tokens: [], fungible_tokens: [{ name: "sbtc-token" }, { name: "sbtc-token-locked" }], @@ -2065,6 +2482,15 @@ export const contracts = { type: "uint128", access: "constant", } as TypedAbiVariable, + withdrawRole: { + name: "withdraw-role", + type: { + buffer: { + length: 1, + }, + }, + access: "constant", + } as TypedAbiVariable, }, constants: { DUST_LIMIT: 546n, @@ -2108,6 +2534,7 @@ export const contracts = { MAX_ADDRESS_VERSION: 6n, mAX_ADDRESS_VERSION_BUFF_20: 4n, mAX_ADDRESS_VERSION_BUFF_32: 6n, + withdrawRole: Uint8Array.from([2]), }, non_fungible_tokens: [], fungible_tokens: [], @@ -2144,6 +2571,8 @@ export const identifiers = { sbtcBootstrapSigners: "ST2SBXRBJJTH7GV5J93HJ62W2NRRQ46XYBK92Y039.sbtc-bootstrap-signers", sbtcDeposit: "ST2SBXRBJJTH7GV5J93HJ62W2NRRQ46XYBK92Y039.sbtc-deposit", + sbtcDepositUpdateTest: + "ST2SBXRBJJTH7GV5J93HJ62W2NRRQ46XYBK92Y039.sbtc-deposit-update-test", sbtcRegistry: "ST2SBXRBJJTH7GV5J93HJ62W2NRRQ46XYBK92Y039.sbtc-registry", sbtcToken: "ST2SBXRBJJTH7GV5J93HJ62W2NRRQ46XYBK92Y039.sbtc-token", sbtcTokenTest: "ST2SBXRBJJTH7GV5J93HJ62W2NRRQ46XYBK92Y039.sbtc-token-test", @@ -2169,6 +2598,14 @@ export const deployments = { testnet: null, mainnet: null, }, + sbtcDepositUpdateTest: { + devnet: + "ST2SBXRBJJTH7GV5J93HJ62W2NRRQ46XYBK92Y039.sbtc-deposit-update-test", + simnet: + "ST2SBXRBJJTH7GV5J93HJ62W2NRRQ46XYBK92Y039.sbtc-deposit-update-test", + testnet: null, + mainnet: null, + }, sbtcRegistry: { devnet: "ST2SBXRBJJTH7GV5J93HJ62W2NRRQ46XYBK92Y039.sbtc-registry", simnet: "ST2SBXRBJJTH7GV5J93HJ62W2NRRQ46XYBK92Y039.sbtc-registry", diff --git a/contracts/tests/helpers.ts b/contracts/tests/helpers.ts index 843d0a5b1..9f32a0e21 100644 --- a/contracts/tests/helpers.ts +++ b/contracts/tests/helpers.ts @@ -28,6 +28,7 @@ export const signers = contracts.sbtcBootstrapSigners; export const withdrawal = contracts.sbtcWithdrawal; export const token = contracts.sbtcToken; export const tokenTest = contracts.sbtcTokenTest; +export const depositUpdate = contracts.sbtcDepositUpdateTest; export const controllerId = `${accounts.deployer.address}.controller`; diff --git a/contracts/tests/sbtc-bootstrap-signers.test.ts b/contracts/tests/sbtc-bootstrap-signers.test.ts index 3878e7b88..e1191f3cb 100644 --- a/contracts/tests/sbtc-bootstrap-signers.test.ts +++ b/contracts/tests/sbtc-bootstrap-signers.test.ts @@ -6,6 +6,10 @@ import { randomPublicKeys, registry, signers, + depositUpdate, + getCurrentBurnInfo, + deposit, + alice, } from "./helpers"; import { test, expect, describe } from "vitest"; import { txOk, txErr, rov, filterEvents } from "@clarigen/test"; @@ -221,4 +225,99 @@ describe("sBTC bootstrap signers contract", () => { }); }); }); + + describe("Update deposit contract", () => { + test("Can update deposit contract & call into new version", () => { + // Switch out active deposit contract + const receipt1 = txOk( + signers.updateProtocolContractWrapper({ + contractType: new Uint8Array([1]), + contractAddress: depositUpdate.identifier, + }), + deployer + ); + expect(receipt1.value).toEqual(true); + // Call into the new contract + const defaultAmount = 1000n; + const defaultMaxFee = 10n; + const { burnHeight, burnHash } = getCurrentBurnInfo(); + const receipt2 = txOk( + depositUpdate.completeDepositWrapper({ + txid: new Uint8Array(32).fill(0), + voutIndex: 0, + amount: defaultAmount + defaultMaxFee, + recipient: alice, + burnHash, + burnHeight, + sweepTxid: new Uint8Array(32).fill(1), + }), + deployer + ); + expect(receipt2.value).toEqual(true); + }); + test("Can not call into previous deposit contract", () => { + // Switch out active deposit contract + const receipt1 = txOk( + signers.updateProtocolContractWrapper({ + contractType: new Uint8Array(1).fill(1), + contractAddress: depositUpdate.identifier, + }), + deployer + ); + expect(receipt1.value).toEqual(true); + const { burnHeight, burnHash } = getCurrentBurnInfo(); + + const receipt = txErr( + deposit.completeDepositWrapper({ + txid: new Uint8Array(32).fill(0), + voutIndex: 0, + amount: 1000n, + recipient: deployer, + burnHash, + burnHeight, + sweepTxid: new Uint8Array(32).fill(1), + }), + deployer + ); + expect(receipt.value).toEqual(errors.registry.ERR_UNAUTHORIZED); + }); + test("Can update deposit contract a second time", () => { + // Switch out active deposit contract + const receipt1 = txOk( + signers.updateProtocolContractWrapper({ + contractType: new Uint8Array(1).fill(1), + contractAddress: depositUpdate.identifier, + }), + deployer + ); + expect(receipt1.value).toEqual(true); + // Call into the new contract + const defaultAmount = 1000n; + const defaultMaxFee = 10n; + const { burnHeight, burnHash } = getCurrentBurnInfo(); + const receipt2 = txOk( + depositUpdate.completeDepositWrapper({ + txid: new Uint8Array(32).fill(0), + voutIndex: 0, + amount: defaultAmount + defaultMaxFee, + recipient: alice, + burnHash, + burnHeight, + sweepTxid: new Uint8Array(32).fill(1), + }), + deployer + ); + expect(receipt2.value).toEqual(true); + // Switch out active deposit contract + const receipt3 = txOk( + signers.updateProtocolContractWrapper({ + contractType: new Uint8Array(1).fill(1), + contractAddress: deposit.identifier, + }), + deployer + ); + expect(receipt3.value).toEqual(true); + }); + }); + }); diff --git a/contracts/tests/sbtc-registry.test.ts b/contracts/tests/sbtc-registry.test.ts index df2482eb3..646e256ac 100644 --- a/contracts/tests/sbtc-registry.test.ts +++ b/contracts/tests/sbtc-registry.test.ts @@ -8,9 +8,13 @@ import { registry, signers, stxAddressToPoxAddress, + deposit, + getCurrentBurnInfo, + token, + withdrawal } from "./helpers"; import { test, expect, describe } from "vitest"; -import { txOk, filterEvents, rov, txErr } from "@clarigen/test"; +import { txOk, filterEvents, rov, txErr, rovOk } from "@clarigen/test"; import { CoreNodeEventType, cvToValue } from "@clarigen/core"; const alicePoxAddr = stxAddressToPoxAddress(alice); @@ -25,45 +29,70 @@ describe("sBTC registry contract", () => { test("Storing a new withdrawal request", () => { const recipient = alicePoxAddr; - const receipt = txOk( - registry.createWithdrawalRequest({ - amount: 100n, - recipient, - maxFee: 10n, - sender: alice, - height: simnet.blockHeight, + const defaultAmount = 1000n; + const defaultMaxFee = 10n; + const { burnHeight, burnHash } = getCurrentBurnInfo(); + txOk( + deposit.completeDepositWrapper({ + txid: new Uint8Array(32).fill(0), + voutIndex: 0, + amount: defaultAmount + defaultMaxFee, + recipient: alice, + burnHash, + burnHeight, + sweepTxid: new Uint8Array(32).fill(1), }), deployer ); - expect(receipt.value).toEqual(1n); - + expect(rovOk(token.getBalance(alice))).toEqual( + defaultAmount + defaultMaxFee + ); + txOk( + withdrawal.initiateWithdrawalRequest({ + amount: defaultAmount, + recipient: alicePoxAddr, + maxFee: defaultMaxFee, + }), + alice + ); const request = getWithdrawalRequest(1n); if (!request) { throw new Error("Request not stored"); } expect(request.sender).toEqual(alice); - expect(request.amount).toEqual(100n); + expect(request.amount).toEqual(defaultAmount); expect(request.maxFee).toEqual(10n); expect(request.recipient).toEqual(recipient); - expect(request.blockHeight).toEqual(BigInt(simnet.blockHeight - 1)); - - // ensure last-id is updated + expect(request.blockHeight).toEqual(BigInt(simnet.burnBlockHeight)); const lastId = getLastWithdrawalRequestId(); expect(lastId).toEqual(1n); }); test("emitted events when a new withdrawal is stored", () => { const recipient = bobPoxAddr; - const receipt = txOk( - registry.createWithdrawalRequest({ - recipient, - amount: 100n, - maxFee: 10n, - sender: bob, - height: simnet.blockHeight, + const defaultAmount = 1000n; + const defaultMaxFee = 10n; + const { burnHeight, burnHash } = getCurrentBurnInfo(); + txOk( + deposit.completeDepositWrapper({ + txid: new Uint8Array(32).fill(0), + voutIndex: 0, + amount: defaultAmount + defaultMaxFee, + recipient: bob, + burnHash, + burnHeight, + sweepTxid: new Uint8Array(32).fill(1), }), deployer ); + const receipt = txOk( + withdrawal.initiateWithdrawalRequest({ + amount: defaultAmount, + recipient: recipient, + maxFee: defaultMaxFee, + }), + bob + ); const prints = filterEvents( receipt.events, CoreNodeEventType.ContractEvent @@ -86,7 +115,7 @@ describe("sBTC registry contract", () => { expect(printData).toStrictEqual({ sender: bob, recipient: recipient, - amount: 100n, + amount: defaultAmount, maxFee: 10n, blockHeight: request.blockHeight, topic: "withdrawal-create", @@ -95,28 +124,44 @@ describe("sBTC registry contract", () => { }); test("get-withdrawal-request includes status", () => { + + const defaultAmount = 1000n; + const defaultMaxFee = 10n; + const { burnHeight, burnHash } = getCurrentBurnInfo(); txOk( - registry.createWithdrawalRequest({ - sender: alice, - recipient: alicePoxAddr, - amount: 100n, - maxFee: 10n, - height: simnet.blockHeight, + deposit.completeDepositWrapper({ + txid: new Uint8Array(32).fill(0), + voutIndex: 0, + amount: defaultAmount + defaultMaxFee, + recipient: alice, + burnHash, + burnHeight, + sweepTxid: new Uint8Array(32).fill(1), }), deployer ); - + expect(rovOk(token.getBalance(alice))).toEqual( + defaultAmount + defaultMaxFee + ); + txOk( + withdrawal.initiateWithdrawalRequest({ + amount: defaultAmount, + recipient: alicePoxAddr, + maxFee: defaultMaxFee, + }), + alice + ); const request = rov(registry.getWithdrawalRequest(1n)); if (!request) { throw new Error("Request not found"); } expect(request.status).toEqual(null); - expect(request).toStrictEqual({ + expect(request).toEqual({ sender: alice, recipient: alicePoxAddr, - amount: 100n, + amount: defaultAmount, maxFee: 10n, - blockHeight: BigInt(simnet.blockHeight - 1), + blockHeight: BigInt(2n), status: null, }); }); diff --git a/contracts/tests/sbtc-token.test.ts b/contracts/tests/sbtc-token.test.ts index d80e8dd82..b95874e57 100644 --- a/contracts/tests/sbtc-token.test.ts +++ b/contracts/tests/sbtc-token.test.ts @@ -394,6 +394,7 @@ describe("sBTC token contract", () => { token.protocolMint({ amount: 1000n, recipient: bob, + contractFlag: new Uint8Array([0]) }), bob ); @@ -405,6 +406,7 @@ describe("sBTC token contract", () => { token.protocolLock({ amount: 1000n, owner: bob, + contractFlag: new Uint8Array([0]) }), bob ); @@ -416,6 +418,7 @@ describe("sBTC token contract", () => { token.protocolBurn({ amount: 1000n, owner: bob, + contractFlag: new Uint8Array([0]) }), bob ); @@ -427,6 +430,7 @@ describe("sBTC token contract", () => { token.protocolBurnLocked({ amount: 1000n, owner: bob, + contractFlag: new Uint8Array([0]) }), bob ); @@ -463,6 +467,7 @@ describe("sBTC token contract", () => { token.protocolMint({ amount: 1000n, recipient: bob, + contractFlag: new Uint8Array([3]) }), deployer ); @@ -476,6 +481,7 @@ describe("sBTC token contract", () => { token.protocolLock({ amount: 222n, owner: bob, + contractFlag: new Uint8Array([3]) }), deployer ); @@ -490,6 +496,7 @@ describe("sBTC token contract", () => { amount: 900n, sender: bob, recipient: charlie, + contractFlag: new Uint8Array([3]) }), deployer ); @@ -501,6 +508,7 @@ describe("sBTC token contract", () => { amount: 500n, sender: bob, recipient: charlie, + contractFlag: new Uint8Array([3]) }), deployer ); @@ -512,6 +520,7 @@ describe("sBTC token contract", () => { token.protocolUnlock({ amount: 500n, owner: bob, + contractFlag: new Uint8Array([3]) }), deployer ); @@ -522,6 +531,7 @@ describe("sBTC token contract", () => { token.protocolUnlock({ amount: 222n, owner: bob, + contractFlag: new Uint8Array([3]) }), deployer );