diff --git a/contracts/Clarinet.toml b/contracts/Clarinet.toml index d1b77a205..94ee715c6 100644 --- a/contracts/Clarinet.toml +++ b/contracts/Clarinet.toml @@ -25,6 +25,10 @@ path = 'contracts/sbtc-withdrawal.clar' clarity_version = 2 epoch = 2.5 +[contracts.sbtc-token] +path = 'contracts/sbtc-token.clar' +clarity_version = 2 +epoch = 2.5 [repl.analysis] passes = ['check_checker'] diff --git a/contracts/contracts/sbtc-deposit.clar b/contracts/contracts/sbtc-deposit.clar index a69ecdd28..32cc590fe 100644 --- a/contracts/contracts/sbtc-deposit.clar +++ b/contracts/contracts/sbtc-deposit.clar @@ -4,6 +4,7 @@ ;; The required length of a txid (define-constant txid-length u32) +(define-constant dust-limit u546) ;; error codes @@ -11,6 +12,7 @@ (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)) ;; data vars @@ -32,14 +34,17 @@ ;; TODO ;; Check that tx-sender is the bootstrap signer + ;; 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) ;; Assert that the deposit has not already been completed (no replay) (asserts! (is-none replay-fetch) ERR_DEPOSIT_REPLAY) - ;; TODO ;; Mint the sBTC to the recipient + (try! (contract-call? .sbtc-token protocol-mint amount recipient)) ;; Complete the deposit (ok (contract-call? .sbtc-registry complete-deposit txid vout-index amount recipient)) diff --git a/contracts/contracts/sbtc-registry.clar b/contracts/contracts/sbtc-registry.clar index da4170f70..1fe4c289e 100644 --- a/contracts/contracts/sbtc-registry.clar +++ b/contracts/contracts/sbtc-registry.clar @@ -59,6 +59,12 @@ ;; stored to avoid replay (define-map multi-sig-address principal 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) +(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 withrawal @@ -171,7 +177,8 @@ (print { topic: "completed-deposit", txid: txid, - vout-index: vout-index + vout-index: vout-index, + amount: amount }) (ok true) ) @@ -218,4 +225,12 @@ ;; wont be hit ;; (if (is-eq contract-caller .controller) (ok true) (err ERR_UNAUTHORIZED)) (if false ERR_UNAUTHORIZED (ok true)) -) \ No newline at end of file +) + +;; Checks whether the contract-caller is a protocol contract +(define-read-only (is-protocol-caller (principal-checked principal)) + (is-some (map-get? protocol-contracts principal-checked)) +) + +;; TODO: Add a function to add a protocol contract +;; TODO: Add a function to remove a protocol contract \ No newline at end of file diff --git a/contracts/contracts/sbtc-token.clar b/contracts/contracts/sbtc-token.clar new file mode 100644 index 000000000..d13cdf50d --- /dev/null +++ b/contracts/contracts/sbtc-token.clar @@ -0,0 +1,146 @@ +(define-constant ERR_NOT_OWNER (err u4)) ;; `tx-sender` or `contract-caller` tried to move a token it does not own. +(define-constant ERR_NOT_AUTH (err u5)) ;; `tx-sender` or `contract-caller` is not the protocol caller + +(define-fungible-token sbtc-token) +(define-fungible-token sbtc-token-locked) + +(define-data-var token-name (string-ascii 32) "sBTC Mini") +(define-data-var token-symbol (string-ascii 10) "sBTC") +(define-data-var token-uri (optional (string-utf8 256)) none) +(define-constant token-decimals u8) + +(define-read-only (is-protocol-caller) + (ok (asserts! (contract-call? .sbtc-registry is-protocol-caller contract-caller) ERR_NOT_AUTH)) +) + +;; --- Protocol functions + +;; #[allow(unchecked_data)] +(define-public (protocol-transfer (amount uint) (sender principal) (recipient principal)) + (begin + (try! (is-protocol-caller)) + (ft-transfer? sbtc-token amount sender recipient) + ) +) + +;; #[allow(unchecked_data)] +(define-public (protocol-lock (amount uint) (owner principal)) + (begin + (try! (is-protocol-caller)) + (try! (ft-burn? sbtc-token amount owner)) + (ft-mint? sbtc-token-locked amount owner) + ) +) + +;; #[allow(unchecked_data)] +(define-public (protocol-unlock (amount uint) (owner principal)) + (begin + (try! (is-protocol-caller)) + (try! (ft-burn? sbtc-token-locked amount owner)) + (ft-mint? sbtc-token amount owner) + ) +) + +;; #[allow(unchecked_data)] +(define-public (protocol-mint (amount uint) (recipient principal)) + (begin + (try! (is-protocol-caller)) + (ft-mint? sbtc-token amount recipient) + ) +) + +;; #[allow(unchecked_data)] +(define-public (protocol-burn (amount uint) (owner principal)) + (begin + (try! (is-protocol-caller)) + (ft-burn? sbtc-token amount owner) + ) +) + +;; #[allow(unchecked_data)] +(define-public (protocol-burn-locked (amount uint) (owner principal)) + (begin + (try! (is-protocol-caller)) + (ft-burn? sbtc-token-locked amount owner) + ) +) + +;; #[allow(unchecked_data)] +(define-public (protocol-set-name (new-name (string-ascii 32))) + (begin + (try! (is-protocol-caller)) + (ok (var-set token-name new-name)) + ) +) + +;; #[allow(unchecked_data)] +(define-public (protocol-set-symbol (new-symbol (string-ascii 10))) + (begin + (try! (is-protocol-caller)) + (ok (var-set token-symbol new-symbol)) + ) +) + +;; #[allow(unchecked_data)] +(define-public (protocol-set-token-uri (new-uri (optional (string-utf8 256)))) + (begin + (try! (is-protocol-caller)) + (ok (var-set token-uri new-uri)) + ) +) + +(define-private (protocol-mint-many-iter (item {amount: uint, recipient: principal})) + (ft-mint? sbtc-token (get amount item) (get recipient item)) +) + +;; #[allow(unchecked_data)] +(define-public (protocol-mint-many (recipients (list 200 {amount: uint, recipient: principal}))) + (begin + (try! (is-protocol-caller)) + (ok (map protocol-mint-many-iter recipients)) + ) +) + +;; --- Public functions + +;; sip-010-trait + +;; #[allow(unchecked_data)] +(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34)))) + (begin + (asserts! (or (is-eq tx-sender sender) (is-eq contract-caller sender)) ERR_NOT_OWNER) + (ft-transfer? sbtc-token amount sender recipient) + ) +) + +(define-read-only (get-name) + (ok (var-get token-name)) +) + +(define-read-only (get-symbol) + (ok (var-get token-symbol)) +) + +(define-read-only (get-decimals) + (ok token-decimals) +) + +(define-read-only (get-balance (who principal)) + (ok (+ (ft-get-balance sbtc-token who) (ft-get-balance sbtc-token-locked who))) +) + +(define-read-only (get-balance-available (who principal)) + (ok (ft-get-balance sbtc-token who)) +) + +(define-read-only (get-balance-locked (who principal)) + (ok (ft-get-balance sbtc-token-locked who)) +) + +(define-read-only (get-total-supply) + (ok (+ (ft-get-supply sbtc-token) (ft-get-supply sbtc-token-locked))) +) + +(define-read-only (get-token-uri) + (ok (var-get token-uri)) +) \ No newline at end of file diff --git a/contracts/deployments/default.simnet-plan.yaml b/contracts/deployments/default.simnet-plan.yaml index d74b6c094..c4a9ef062 100644 --- a/contracts/deployments/default.simnet-plan.yaml +++ b/contracts/deployments/default.simnet-plan.yaml @@ -59,6 +59,11 @@ plan: emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM path: contracts/sbtc-bootstrap-signers.clar clarity-version: 2 + - emulated-contract-publish: + contract-name: sbtc-token + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/sbtc-token.clar + clarity-version: 2 - emulated-contract-publish: contract-name: sbtc-deposit emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM diff --git a/contracts/docs/sbtc-deposit.md b/contracts/docs/sbtc-deposit.md deleted file mode 100644 index 9afc1e3f9..000000000 --- a/contracts/docs/sbtc-deposit.md +++ /dev/null @@ -1,112 +0,0 @@ -# sbtc-deposit - -[`sbtc-deposit.clar`](../contracts/sbtc-deposit.clar) - -sBTC Deposit contract - -**Public functions:** - -- [`complete-deposit-wrapper`](#complete-deposit-wrapper) - -**Read-only functions:** - -**Private functions:** - -**Maps** - -**Variables** - -**Constants** - -- [`txid-length`](#txid-length) -- [`ERR_TXID_LEN`](#err_txid_len) -- [`ERR_DEPOSIT_REPLAY`](#err_deposit_replay) - -## Functions - -### complete-deposit-wrapper - -[View in file](../contracts/sbtc-deposit.clar#L26) - -`(define-public (complete-deposit-wrapper ((txid (buff 32)) (vout-index uint) (amount uint) (recipient principal)) (response (response bool uint) uint))` - -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 - -
- Source code: - -```clarity -(define-public (complete-deposit-wrapper (txid (buff 32)) (vout-index uint) (amount uint) (recipient principal)) - (let - ( - (replay-fetch (contract-call? .sbtc-registry get-completed-deposit txid vout-index)) - ) - - ;; TODO - ;; Check that tx-sender is the bootstrap signer - - ;; Check that txid is the correct length - (asserts! (is-eq (len 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) - - ;; TODO - ;; Mint the sBTC to the recipient - - ;; Complete the deposit - (ok (contract-call? .sbtc-registry complete-deposit txid vout-index amount recipient)) - ) -) -``` - -
- -**Parameters:** - -| Name | Type | -| ---------- | --------- | -| txid | (buff 32) | -| vout-index | uint | -| amount | uint | -| recipient | principal | - -## Maps - -## Variables - -## Constants - -### txid-length - -The required length of a txid - -```clarity -(define-constant txid-length u32) -``` - -[View in file](../contracts/sbtc-deposit.clar#L6) - -### ERR_TXID_LEN - -TXID used in deposit is not the correct length - -```clarity -(define-constant ERR_TXID_LEN (err u300)) -``` - -[View in file](../contracts/sbtc-deposit.clar#L11) - -### ERR_DEPOSIT_REPLAY - -Deposit has already been completed - -```clarity -(define-constant ERR_DEPOSIT_REPLAY (err u301)) -``` - -[View in file](../contracts/sbtc-deposit.clar#L13) diff --git a/contracts/docs/sbtc-registry.md b/contracts/docs/sbtc-registry.md deleted file mode 100644 index 7db74f82f..000000000 --- a/contracts/docs/sbtc-registry.md +++ /dev/null @@ -1,554 +0,0 @@ -# sbtc-registry - -[`sbtc-registry.clar`](../contracts/sbtc-registry.clar) - -sBTC Registry contract - -**Public functions:** - -- [`create-withdrawal-request`](#create-withdrawal-request) -- [`complete-deposit`](#complete-deposit) -- [`rotate-keys`](#rotate-keys) - -**Read-only functions:** - -- [`get-withdrawal-request`](#get-withdrawal-request) -- [`get-completed-deposit`](#get-completed-deposit) -- [`get-current-signer-data`](#get-current-signer-data) -- [`get-current-aggregate-pubkey`](#get-current-aggregate-pubkey) -- [`get-current-signer-principal`](#get-current-signer-principal) -- [`get-current-signer-set`](#get-current-signer-set) - -**Private functions:** - -- [`increment-last-withdrawal-request-id`](#increment-last-withdrawal-request-id) -- [`validate-caller`](#validate-caller) - -**Maps** - -- [`withdrawal-requests`](#withdrawal-requests) -- [`withdrawal-status`](#withdrawal-status) -- [`completed-deposits`](#completed-deposits) -- [`aggregate-pubkeys`](#aggregate-pubkeys) -- [`multi-sig-address`](#multi-sig-address) - -**Variables** - -- [`last-withdrawal-request-id`](#last-withdrawal-request-id) -- [`current-signer-set`](#current-signer-set) -- [`current-aggregate-pubkey`](#current-aggregate-pubkey) -- [`current-signer-principal`](#current-signer-principal) - -**Constants** - -- [`ERR_UNAUTHORIZED`](#err_unauthorized) -- [`ERR_INVALID_REQUEST_ID`](#err_invalid_request_id) -- [`ERR_AGG_PUBKEY_REPLAY`](#err_agg_pubkey_replay) -- [`ERR_MULTI_SIG_REPLAY`](#err_multi_sig_replay) - -## Functions - -### get-withdrawal-request - -[View in file](../contracts/sbtc-registry.clar#L66) - -`(define-read-only (get-withdrawal-request ((id uint)) (optional (tuple (amount uint) (block-height uint) (max-fee uint) (recipient (tuple (hashbytes (buff 32)) (version (buff 1)))) (sender principal) (status (optional bool)))))` - -Read-only functions -Get a withdrawal request by its ID. -This function returns the fields of the withrawal -request, along with its status. - -
- Source code: - -```clarity -(define-read-only (get-withdrawal-request (id uint)) - (match (map-get? withdrawal-requests id) - request (some (merge request { - status: (map-get? withdrawal-status id) - })) - none - ) -) -``` - -
- -**Parameters:** - -| Name | Type | -| ---- | ---- | -| id | uint | - -### get-completed-deposit - -[View in file](../contracts/sbtc-registry.clar#L77) - -`(define-read-only (get-completed-deposit ((txid (buff 32)) (vout-index uint)) (optional (tuple (amount uint) (recipient principal))))` - -Get a completed deposit by its transaction ID & vout index. -This function returns the fields of the completed-deposits map. - -
- Source code: - -```clarity -(define-read-only (get-completed-deposit (txid (buff 32)) (vout-index uint)) - (map-get? completed-deposits {txid: txid, vout-index: vout-index}) -) -``` - -
- -**Parameters:** - -| Name | Type | -| ---------- | --------- | -| txid | (buff 32) | -| vout-index | uint | - -### get-current-signer-data - -[View in file](../contracts/sbtc-registry.clar#L83) - -`(define-read-only (get-current-signer-data () (tuple (current-aggregate-pubkey (buff 33)) (current-signer-principal principal) (current-signer-set (list 15 (buff 33)))))` - -Get the current signer set. -This function returns the current signer set as a list of principals. - -
- Source code: - -```clarity -(define-read-only (get-current-signer-data) - { - current-signer-set: (var-get current-signer-set), - current-aggregate-pubkey: (var-get current-aggregate-pubkey), - current-signer-principal: (var-get current-signer-principal) - } -) -``` - -
- -### get-current-aggregate-pubkey - -[View in file](../contracts/sbtc-registry.clar#L93) - -`(define-read-only (get-current-aggregate-pubkey () (buff 33))` - -Get the current aggregate pubkey. -This function returns the current aggregate pubkey. - -
- Source code: - -```clarity -(define-read-only (get-current-aggregate-pubkey) - (var-get current-aggregate-pubkey) -) -``` - -
- -### get-current-signer-principal - -[View in file](../contracts/sbtc-registry.clar#L99) - -`(define-read-only (get-current-signer-principal () principal)` - -Get the current signer principal. -This function returns the current signer principal. - -
- Source code: - -```clarity -(define-read-only (get-current-signer-principal) - (var-get current-signer-principal) -) -``` - -
- -### get-current-signer-set - -[View in file](../contracts/sbtc-registry.clar#L103) - -`(define-read-only (get-current-signer-set () (list 15 (buff 33)))` - -
- Source code: - -```clarity -(define-read-only (get-current-signer-set) - (var-get current-signer-set) -) -``` - -
- -### create-withdrawal-request - -[View in file](../contracts/sbtc-registry.clar#L119) - -`(define-public (create-withdrawal-request ((amount uint) (max-fee uint) (sender principal) (recipient (tuple (hashbytes (buff 32)) (version (buff 1)))) (height uint)) (response uint uint))` - -Store a new withdrawal request. -Note that this function can only be called by other sBTC -contracts - it cannot be called by users directly. - -This function does not handle validation or moving the funds. -Instead, it is purely for the purpose of storing the request. - -The function will emit a print event with the topic "withdrawal-request" -and the data of the request. - -
- Source code: - -```clarity -(define-public (create-withdrawal-request - (amount uint) - (max-fee uint) - (sender principal) - (recipient { version: (buff 1), hashbytes: (buff 32) }) - (height uint) - ) - (let - ( - (id (increment-last-withdrawal-request-id)) - ) - (try! (validate-caller)) - ;; #[allow(unchecked_data)] - (map-insert withdrawal-requests id { - amount: amount, - max-fee: max-fee, - sender: sender, - recipient: recipient, - block-height: height, - }) - (print { - topic: "withdrawal-request", - amount: amount, - request-id: id, - sender: sender, - recipient: recipient, - block-height: height, - max-fee: max-fee, - }) - (ok id) - ) -) -``` - -
- -**Parameters:** - -| Name | Type | -| --------- | ------------------------------------------------ | -| amount | uint | -| max-fee | uint | -| sender | principal | -| recipient | (tuple (hashbytes (buff 32)) (version (buff 1))) | -| height | uint | - -### complete-deposit - -[View in file](../contracts/sbtc-registry.clar#L159) - -`(define-public (complete-deposit ((txid (buff 32)) (vout-index uint) (amount uint) (recipient principal)) (response bool uint))` - -Store a new insert request. -Note that this function can only be called by other sBTC -contracts (specifically the current version of the deposit contract) - -- it cannot be called by users directly. - -This function does not handle validation or moving the funds. -Instead, it is purely for the purpose of storing the completed deposit. - -
- Source code: - -```clarity -(define-public (complete-deposit - (txid (buff 32)) - (vout-index uint) - (amount uint) - (recipient principal) - ) - (begin - (try! (validate-caller)) - (map-insert completed-deposits {txid: txid, vout-index: vout-index} { - amount: amount, - recipient: recipient - }) - (print { - topic: "completed-deposit", - txid: txid, - vout-index: vout-index - }) - (ok true) - ) -) -``` - -
- -**Parameters:** - -| Name | Type | -| ---------- | --------- | -| txid | (buff 32) | -| vout-index | uint | -| amount | uint | -| recipient | principal | - -### rotate-keys - -[View in file](../contracts/sbtc-registry.clar#L182) - -`(define-public (rotate-keys ((new-keys (list 15 (buff 33))) (new-address principal) (new-aggregate-pubkey (buff 33))) (response bool uint))` - -Rotate the signer set, multi-sig principal, & aggregate pubkey -This function can only be called by the bootstrap-signers contract. - -
- Source code: - -```clarity -(define-public (rotate-keys (new-keys (list 15 (buff 33))) (new-address principal) (new-aggregate-pubkey (buff 33))) - (begin - ;; Check that caller is protocol contract - (try! (validate-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) - ;; Check that the new address (multi-sig) is not already in the map - (asserts! (map-insert multi-sig-address new-address true) ERR_MULTI_SIG_REPLAY) - ;; Update the current signer set - (var-set current-signer-set new-keys) - ;; Update the current multi-sig address - (var-set current-signer-principal new-address) - ;; Update the current aggregate pubkey - (ok (var-set current-aggregate-pubkey new-aggregate-pubkey)) - ) -) -``` - -
- -**Parameters:** - -| Name | Type | -| -------------------- | ------------------- | -| new-keys | (list 15 (buff 33)) | -| new-address | principal | -| new-aggregate-pubkey | (buff 33) | - -### increment-last-withdrawal-request-id - -[View in file](../contracts/sbtc-registry.clar#L203) - -`(define-private (increment-last-withdrawal-request-id () uint)` - -Increment the last withdrawal request ID and -return the new value. - -
- Source code: - -```clarity -(define-private (increment-last-withdrawal-request-id) - (let - ( - (next-value (+ u1 (var-get last-withdrawal-request-id))) - ) - (var-set last-withdrawal-request-id next-value) - next-value - ) -) -``` - -
- -### validate-caller - -[View in file](../contracts/sbtc-registry.clar#L216) - -`(define-private (validate-caller () (response bool uint))` - -Validate the caller of the function. -TODO: Once other contracts are in place, update this -to use the sBTC controller. - -
- Source code: - -```clarity -(define-private (validate-caller) - ;; To provide an explicit error type, add a branch that - ;; wont be hit - ;; (if (is-eq contract-caller .controller) (ok true) (err ERR_UNAUTHORIZED)) - (if false ERR_UNAUTHORIZED (ok true)) -) -``` - -
- -## Maps - -### withdrawal-requests - -Internal data structure to store withdrawal -requests. Requests are associated with a unique -request ID. - -```clarity -(define-map withdrawal-requests uint { - ;; Amount of sBTC being withdrawaled (in sats) - amount: uint, - max-fee: uint, - sender: principal, - ;; BTC recipient address in the same format of - ;; pox contracts - recipient: { - version: (buff 1), - hashbytes: (buff 32), - }, - ;; Burn block height where the withdrawal request was - ;; created - block-height: uint, -}) -``` - -[View in file](../contracts/sbtc-registry.clar#L23) - -### withdrawal-status - -Data structure to map request-id to status -If status is `none`, the request is pending. -Otherwise, the boolean value indicates whether -the deposit was accepted. - -```clarity -(define-map withdrawal-status uint bool) -``` - -[View in file](../contracts/sbtc-registry.clar#L43) - -### completed-deposits - -Internal data structure to store completed -deposit requests & avoid replay attacks. - -```clarity -(define-map completed-deposits {txid: (buff 32), vout-index: uint} - { - amount: uint, - recipient: principal - } -) -``` - -[View in file](../contracts/sbtc-registry.clar#L47) - -### aggregate-pubkeys - -Data structure to store aggregate pubkey, -stored to avoid replay - -```clarity -(define-map aggregate-pubkeys (buff 33) bool) -``` - -[View in file](../contracts/sbtc-registry.clar#L56) - -### multi-sig-address - -Data structure to store the current signer set, -stored to avoid replay - -```clarity -(define-map multi-sig-address principal bool) -``` - -[View in file](../contracts/sbtc-registry.clar#L60) - -## Variables - -### last-withdrawal-request-id - -uint - -```clarity -(define-data-var last-withdrawal-request-id uint u0) -``` - -[View in file](../contracts/sbtc-registry.clar#L12) - -### current-signer-set - -(list 15 (buff 33)) - -```clarity -(define-data-var current-signer-set (list 15 (buff 33)) (list)) -``` - -[View in file](../contracts/sbtc-registry.clar#L13) - -### current-aggregate-pubkey - -(buff 33) - -```clarity -(define-data-var current-aggregate-pubkey (buff 33) 0x00) -``` - -[View in file](../contracts/sbtc-registry.clar#L14) - -### current-signer-principal - -principal - -```clarity -(define-data-var current-signer-principal principal tx-sender) -``` - -[View in file](../contracts/sbtc-registry.clar#L15) - -## Constants - -### ERR_UNAUTHORIZED - -```clarity -(define-constant ERR_UNAUTHORIZED (err u400)) -``` - -[View in file](../contracts/sbtc-registry.clar#L5) - -### ERR_INVALID_REQUEST_ID - -```clarity -(define-constant ERR_INVALID_REQUEST_ID (err u401)) -``` - -[View in file](../contracts/sbtc-registry.clar#L6) - -### ERR_AGG_PUBKEY_REPLAY - -```clarity -(define-constant ERR_AGG_PUBKEY_REPLAY (err u402)) -``` - -[View in file](../contracts/sbtc-registry.clar#L7) - -### ERR_MULTI_SIG_REPLAY - -```clarity -(define-constant ERR_MULTI_SIG_REPLAY (err u403)) -``` - -[View in file](../contracts/sbtc-registry.clar#L8) diff --git a/contracts/tests/clarigen-types.ts b/contracts/tests/clarigen-types.ts index fdc0c09ad..95e1629e4 100644 --- a/contracts/tests/clarigen-types.ts +++ b/contracts/tests/clarigen-types.ts @@ -517,6 +517,16 @@ export const contracts = { }, 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: { @@ -527,6 +537,11 @@ export const contracts = { }, access: "constant", } as TypedAbiVariable>, + dustLimit: { + name: "dust-limit", + type: "uint128", + access: "constant", + } as TypedAbiVariable, txidLength: { name: "txid-length", type: "uint128", @@ -538,10 +553,15 @@ export const contracts = { isOk: false, value: 301n, }, + ERR_LOWER_THAN_DUST: { + isOk: false, + value: 302n, + }, ERR_TXID_LEN: { isOk: false, value: 300n, }, + dustLimit: 546n, txidLength: 32n, }, non_fungible_tokens: [], @@ -754,6 +774,15 @@ export const contracts = { status: boolean | null; } | null >, + isProtocolCaller: { + name: "is-protocol-caller", + access: "read_only", + args: [{ name: "principal-checked", type: "principal" }], + outputs: { type: "bool" }, + } as TypedAbiFunction< + [principalChecked: TypedAbiArg], + boolean + >, }, maps: { aggregatePubkeys: { @@ -790,6 +819,11 @@ export const contracts = { key: "principal", value: "bool", } as TypedAbiMap, + protocolContracts: { + name: "protocol-contracts", + key: "principal", + value: "bool", + } as TypedAbiMap, withdrawalRequests: { name: "withdrawal-requests", key: "uint128", @@ -932,6 +966,380 @@ export const contracts = { clarity_version: "Clarity2", contractName: "sbtc-registry", }, + sbtcToken: { + functions: { + protocolMintManyIter: { + name: "protocol-mint-many-iter", + access: "private", + args: [ + { + name: "item", + type: { + tuple: [ + { name: "amount", type: "uint128" }, + { name: "recipient", type: "principal" }, + ], + }, + }, + ], + outputs: { type: { response: { ok: "bool", error: "uint128" } } }, + } as TypedAbiFunction< + [ + item: TypedAbiArg< + { + amount: number | bigint; + recipient: string; + }, + "item" + >, + ], + Response + >, + protocolBurn: { + name: "protocol-burn", + access: "public", + args: [ + { name: "amount", type: "uint128" }, + { name: "owner", type: "principal" }, + ], + outputs: { type: { response: { ok: "bool", error: "uint128" } } }, + } as TypedAbiFunction< + [ + amount: TypedAbiArg, + owner: TypedAbiArg, + ], + Response + >, + protocolBurnLocked: { + name: "protocol-burn-locked", + access: "public", + args: [ + { name: "amount", type: "uint128" }, + { name: "owner", type: "principal" }, + ], + outputs: { type: { response: { ok: "bool", error: "uint128" } } }, + } as TypedAbiFunction< + [ + amount: TypedAbiArg, + owner: TypedAbiArg, + ], + Response + >, + protocolLock: { + name: "protocol-lock", + access: "public", + args: [ + { name: "amount", type: "uint128" }, + { name: "owner", type: "principal" }, + ], + outputs: { type: { response: { ok: "bool", error: "uint128" } } }, + } as TypedAbiFunction< + [ + amount: TypedAbiArg, + owner: TypedAbiArg, + ], + Response + >, + protocolMint: { + name: "protocol-mint", + access: "public", + args: [ + { name: "amount", type: "uint128" }, + { name: "recipient", type: "principal" }, + ], + outputs: { type: { response: { ok: "bool", error: "uint128" } } }, + } as TypedAbiFunction< + [ + amount: TypedAbiArg, + recipient: TypedAbiArg, + ], + Response + >, + protocolMintMany: { + name: "protocol-mint-many", + access: "public", + args: [ + { + name: "recipients", + type: { + list: { + type: { + tuple: [ + { name: "amount", type: "uint128" }, + { name: "recipient", type: "principal" }, + ], + }, + length: 200, + }, + }, + }, + ], + outputs: { + type: { + response: { + ok: { + list: { + type: { response: { ok: "bool", error: "uint128" } }, + length: 200, + }, + }, + error: "uint128", + }, + }, + }, + } as TypedAbiFunction< + [ + recipients: TypedAbiArg< + { + amount: number | bigint; + recipient: string; + }[], + "recipients" + >, + ], + Response[], bigint> + >, + protocolSetName: { + name: "protocol-set-name", + access: "public", + args: [{ name: "new-name", type: { "string-ascii": { length: 32 } } }], + outputs: { type: { response: { ok: "bool", error: "uint128" } } }, + } as TypedAbiFunction< + [newName: TypedAbiArg], + Response + >, + protocolSetSymbol: { + name: "protocol-set-symbol", + access: "public", + args: [ + { name: "new-symbol", type: { "string-ascii": { length: 10 } } }, + ], + outputs: { type: { response: { ok: "bool", error: "uint128" } } }, + } as TypedAbiFunction< + [newSymbol: TypedAbiArg], + Response + >, + protocolSetTokenUri: { + name: "protocol-set-token-uri", + access: "public", + args: [ + { + name: "new-uri", + type: { optional: { "string-utf8": { length: 256 } } }, + }, + ], + outputs: { type: { response: { ok: "bool", error: "uint128" } } }, + } as TypedAbiFunction< + [newUri: TypedAbiArg], + Response + >, + protocolTransfer: { + name: "protocol-transfer", + access: "public", + args: [ + { name: "amount", type: "uint128" }, + { name: "sender", type: "principal" }, + { name: "recipient", type: "principal" }, + ], + outputs: { type: { response: { ok: "bool", error: "uint128" } } }, + } as TypedAbiFunction< + [ + amount: TypedAbiArg, + sender: TypedAbiArg, + recipient: TypedAbiArg, + ], + Response + >, + protocolUnlock: { + name: "protocol-unlock", + access: "public", + args: [ + { name: "amount", type: "uint128" }, + { name: "owner", type: "principal" }, + ], + outputs: { type: { response: { ok: "bool", error: "uint128" } } }, + } as TypedAbiFunction< + [ + amount: TypedAbiArg, + owner: TypedAbiArg, + ], + Response + >, + transfer: { + name: "transfer", + access: "public", + args: [ + { name: "amount", type: "uint128" }, + { name: "sender", type: "principal" }, + { name: "recipient", type: "principal" }, + { name: "memo", type: { optional: { buffer: { length: 34 } } } }, + ], + outputs: { type: { response: { ok: "bool", error: "uint128" } } }, + } as TypedAbiFunction< + [ + amount: TypedAbiArg, + sender: TypedAbiArg, + recipient: TypedAbiArg, + memo: TypedAbiArg, + ], + Response + >, + getBalance: { + name: "get-balance", + access: "read_only", + args: [{ name: "who", type: "principal" }], + outputs: { type: { response: { ok: "uint128", error: "none" } } }, + } as TypedAbiFunction< + [who: TypedAbiArg], + Response + >, + getBalanceAvailable: { + name: "get-balance-available", + access: "read_only", + args: [{ name: "who", type: "principal" }], + outputs: { type: { response: { ok: "uint128", error: "none" } } }, + } as TypedAbiFunction< + [who: TypedAbiArg], + Response + >, + getBalanceLocked: { + name: "get-balance-locked", + access: "read_only", + args: [{ name: "who", type: "principal" }], + outputs: { type: { response: { ok: "uint128", error: "none" } } }, + } as TypedAbiFunction< + [who: TypedAbiArg], + Response + >, + getDecimals: { + name: "get-decimals", + access: "read_only", + args: [], + outputs: { type: { response: { ok: "uint128", error: "none" } } }, + } as TypedAbiFunction<[], Response>, + getName: { + name: "get-name", + access: "read_only", + args: [], + outputs: { + type: { + response: { ok: { "string-ascii": { length: 32 } }, error: "none" }, + }, + }, + } as TypedAbiFunction<[], Response>, + getSymbol: { + name: "get-symbol", + access: "read_only", + args: [], + outputs: { + type: { + response: { ok: { "string-ascii": { length: 10 } }, error: "none" }, + }, + }, + } as TypedAbiFunction<[], Response>, + getTokenUri: { + name: "get-token-uri", + access: "read_only", + args: [], + outputs: { + type: { + response: { + ok: { optional: { "string-utf8": { length: 256 } } }, + error: "none", + }, + }, + }, + } as TypedAbiFunction<[], Response>, + getTotalSupply: { + name: "get-total-supply", + access: "read_only", + args: [], + outputs: { type: { response: { ok: "uint128", error: "none" } } }, + } as TypedAbiFunction<[], Response>, + isProtocolCaller: { + name: "is-protocol-caller", + access: "read_only", + args: [], + outputs: { type: { response: { ok: "bool", error: "uint128" } } }, + } as TypedAbiFunction<[], Response>, + }, + maps: {}, + variables: { + ERR_NOT_AUTH: { + name: "ERR_NOT_AUTH", + type: { + response: { + ok: "none", + error: "uint128", + }, + }, + access: "constant", + } as TypedAbiVariable>, + ERR_NOT_OWNER: { + name: "ERR_NOT_OWNER", + type: { + response: { + ok: "none", + error: "uint128", + }, + }, + access: "constant", + } as TypedAbiVariable>, + tokenDecimals: { + name: "token-decimals", + type: "uint128", + access: "constant", + } as TypedAbiVariable, + tokenName: { + name: "token-name", + type: { + "string-ascii": { + length: 32, + }, + }, + access: "variable", + } as TypedAbiVariable, + tokenSymbol: { + name: "token-symbol", + type: { + "string-ascii": { + length: 10, + }, + }, + access: "variable", + } as TypedAbiVariable, + tokenUri: { + name: "token-uri", + type: { + optional: { + "string-utf8": { + length: 256, + }, + }, + }, + access: "variable", + } as TypedAbiVariable, + }, + constants: { + ERR_NOT_AUTH: { + isOk: false, + value: 5n, + }, + ERR_NOT_OWNER: { + isOk: false, + value: 4n, + }, + tokenDecimals: 8n, + tokenName: "sBTC Mini", + tokenSymbol: "sBTC", + tokenUri: null, + }, + non_fungible_tokens: [], + fungible_tokens: [{ name: "sbtc-token" }, { name: "sbtc-token-locked" }], + epoch: "Epoch25", + clarity_version: "Clarity2", + contractName: "sbtc-token", + }, sbtcWithdrawal: { functions: { initiateWithdrawalRequest: { @@ -1100,6 +1508,7 @@ export const identifiers = { "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sbtc-bootstrap-signers", sbtcDeposit: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sbtc-deposit", sbtcRegistry: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sbtc-registry", + sbtcToken: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sbtc-token", sbtcWithdrawal: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sbtc-withdrawal", } as const; @@ -1128,6 +1537,12 @@ export const deployments = { testnet: null, mainnet: null, }, + sbtcToken: { + devnet: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sbtc-token", + simnet: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sbtc-token", + testnet: null, + mainnet: null, + }, sbtcWithdrawal: { devnet: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sbtc-withdrawal", simnet: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sbtc-withdrawal", diff --git a/contracts/tests/helpers.ts b/contracts/tests/helpers.ts index 57ccd7135..2b706be7b 100644 --- a/contracts/tests/helpers.ts +++ b/contracts/tests/helpers.ts @@ -26,6 +26,7 @@ export const registry = contracts.sbtcRegistry; export const deposit = contracts.sbtcDeposit; export const signers = contracts.sbtcBootstrapSigners; export const withdrawal = contracts.sbtcWithdrawal; +export const token = contracts.sbtcToken; export const controllerId = `${accounts.deployer.address}.controller`; @@ -36,6 +37,7 @@ export const errors = { deposit: _errors.sbtcDeposit, signers: _errors.sbtcBootstrapSigners, withdrawal: _errors.sbtcWithdrawal, + token: _errors.sbtcToken, }; export function getLastWithdrawalRequestId() { diff --git a/contracts/tests/sbtc-deposit.test.ts b/contracts/tests/sbtc-deposit.test.ts index 7aaa7fbcf..84936c348 100644 --- a/contracts/tests/sbtc-deposit.test.ts +++ b/contracts/tests/sbtc-deposit.test.ts @@ -10,7 +10,7 @@ describe("sBTC deposit contract", () => { deposit.completeDepositWrapper({ txid: new Uint8Array(31).fill(0), voutIndex: 0, - amount: 0, + amount: 1000n, recipient: alice, }), alice @@ -18,12 +18,25 @@ describe("sBTC deposit contract", () => { expect(receipt.value).toEqual(errors.deposit.ERR_TXID_LEN); }); + test("Fail complete-deposit-wrapper invalid low amount", () => { + const receipt = txErr( + deposit.completeDepositWrapper({ + txid: new Uint8Array(32).fill(0), + voutIndex: 0, + amount: 10n, + recipient: alice, + }), + alice + ); + expect(receipt.value).toEqual(deposit.constants.ERR_LOWER_THAN_DUST.value); + }); + test("Fail complete-deposit-wrapper replay deposit (err 301)", () => { const receipt0 = txOk( deposit.completeDepositWrapper({ txid: new Uint8Array(32).fill(0), voutIndex: 0, - amount: 0, + amount: 1000n, recipient: alice, }), alice @@ -33,7 +46,7 @@ describe("sBTC deposit contract", () => { deposit.completeDepositWrapper({ txid: new Uint8Array(32).fill(0), voutIndex: 0, - amount: 0, + amount: 1000n, recipient: alice, }), alice @@ -46,7 +59,7 @@ describe("sBTC deposit contract", () => { deposit.completeDepositWrapper({ txid: new Uint8Array(32).fill(0), voutIndex: 0, - amount: 0, + amount: 1000n, recipient: alice, }), alice @@ -60,11 +73,13 @@ describe("sBTC deposit contract", () => { topic: string; txid: string; voutIndex: bigint; + amount: bigint; }>(print.data.value); expect(printData).toStrictEqual({ topic: "completed-deposit", txid: new Uint8Array(32).fill(0), voutIndex: 0n, + amount: 1000n, }); }); @@ -73,7 +88,7 @@ describe("sBTC deposit contract", () => { deposit.completeDepositWrapper({ txid: new Uint8Array(32).fill(0), voutIndex: 0, - amount: 0, + amount: 1000n, recipient: alice, }), alice @@ -86,7 +101,7 @@ describe("sBTC deposit contract", () => { alice ); expect(receipt1).toStrictEqual({ - amount: 0n, + amount: 1000n, recipient: alice, }); }); diff --git a/contracts/tests/sbtc-token.test.ts b/contracts/tests/sbtc-token.test.ts new file mode 100644 index 000000000..b88c9b7e5 --- /dev/null +++ b/contracts/tests/sbtc-token.test.ts @@ -0,0 +1,114 @@ +import { alice, bob, deposit, errors, token } from "./helpers"; +import { test, expect, describe } from "vitest"; +import { txOk, filterEvents, rov, txErr } from "@clarigen/test"; +import { CoreNodeEventType, cvToValue } from "@clarigen/core"; + +describe("sBTC token contract", () => { + describe("token basics", () => { + test("Mint sbtc token, check Alice balance", () => { + const receipt = txOk( + deposit.completeDepositWrapper({ + txid: new Uint8Array(32).fill(0), + voutIndex: 0, + amount: 1000n, + recipient: alice, + }), + alice + ); + const printEvents = filterEvents( + receipt.events, + CoreNodeEventType.ContractEvent + ); + const [print] = printEvents; + const printData = cvToValue<{ + topic: string; + txid: string; + voutIndex: bigint; + amount: bigint; + }>(print.data.value); + expect(printData).toStrictEqual({ + topic: "completed-deposit", + txid: new Uint8Array(32).fill(0), + voutIndex: 0n, + amount: 1000n, + }); + const receipt1 = rov( + token.getBalance({ + who: alice, + }), + alice + ); + expect(receipt1.value).toEqual(1000n); + }); + + test("Mint & transfer sbtc token, check Bob balance", () => { + const receipt = txOk( + deposit.completeDepositWrapper({ + txid: new Uint8Array(32).fill(0), + voutIndex: 0, + amount: 1000n, + recipient: alice, + }), + alice + ); + const printEvents = filterEvents( + receipt.events, + CoreNodeEventType.ContractEvent + ); + const [print] = printEvents; + const printData = cvToValue<{ + topic: string; + txid: string; + voutIndex: bigint; + amount: bigint; + }>(print.data.value); + expect(printData).toStrictEqual({ + topic: "completed-deposit", + txid: new Uint8Array(32).fill(0), + voutIndex: 0n, + amount: 1000n, + }); + const receipt1 = txOk( + token.transfer({ + amount: 999n, + sender: alice, + recipient: bob, + memo: new Uint8Array(1).fill(0), + }), + alice + ); + expect(receipt1.value).toEqual(true); + const receipt2 = rov( + token.getBalance({ + who: bob, + }), + bob + ); + expect(receipt2.value).toEqual(999n); + }); + + test("Fail a non-protocol principal calling protocol-mint", () => { + const receipt = txErr( + token.protocolMint({ + amount: 1000n, + recipient: bob, + }), + bob + ); + expect(receipt.value).toEqual(errors.token.ERR_NOT_AUTH); + }); + + test("Fail transferring sbtc when not owner", () => { + const receipt1 = txErr( + token.transfer({ + amount: 999n, + sender: alice, + recipient: bob, + memo: new Uint8Array(1).fill(0), + }), + bob + ); + expect(receipt1.value).toEqual(errors.token.ERR_NOT_OWNER); + }); + }); +});