diff --git a/.gitignore b/.gitignore index cd665fd..443ab3a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ .cache/** history.txt node_modules -lcov.info \ No newline at end of file +lcov.info +.aider* +.env diff --git a/Clarinet.toml b/Clarinet.toml index 99c322d..59df76c 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -5,12 +5,27 @@ authors = [] telemetry = true cache_dir = './.cache' +[repl.analysis] +passes = [] + +[repl.analysis.check_checker] +strict = false +trusted_sender = false +trusted_caller = false +callee_filter = false + +# external contracts loaded by clarinet + [[project.requirements]] contract_id = 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait' -[contracts.aibtcdev-aibtc] -path = 'contracts/aibtcdev-aibtc.clar' -clarity_version = 2 -epoch = 2.4 + +[[project.requirements]] +contract_id = 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard' + +[[project.requirements]] +contract_id = 'ST000000000000000000002AMW42H.pox-4' + +# airdrop nft contracts [contracts.aibtcdev-airdrop-1] path = 'contracts/aibtcdev-airdrop-1.clar' @@ -22,46 +37,88 @@ path = 'contracts/aibtcdev-airdrop-2.clar' clarity_version = 2 epoch = 2.5 -[contracts.aibtcdev-bank-account] -path = 'contracts/aibtcdev-bank-account.clar' +# dao base contract + +[contracts.aibtcdev-base-dao] +path = 'contracts/dao/aibtcdev-base-dao.clar' clarity_version = 2 epoch = 2.5 -[contracts.aibtcdev-resources-v1] -path = 'contracts/aibtcdev-resources-v1.clar' +# dao proposals + +[contracts.aibtc-prop001-bootstrap] +path = 'contracts/dao/proposals/aibtc-prop001-bootstrap.clar' clarity_version = 2 -epoch = 2.4 +epoch = 2.5 -[contracts.aibtcdev-traits-v1] -path = 'contracts/aibtcdev-traits-v1.clar' +# dao extensions + +[contracts.aibtc-ext001-actions] +path = 'contracts/dao/extensions/aibtc-ext001-actions.clar' clarity_version = 2 -epoch = 2.4 +epoch = 2.5 -[contracts.external-proxy] -path = 'contracts/test-proxy.clar' -deployer = 'wallet_1' +[contracts.aibtc-ext002-bank-account] +path = 'contracts/dao/extensions/aibtc-ext002-bank-account.clar' +clarity_version = 2 +epoch = 2.5 + +[contracts.aibtc-ext003-direct-execute] +path = 'contracts/dao/extensions/aibtc-ext003-direct-execute.clar' +clarity_version = 2 +epoch = 2.5 + +[contracts.aibtc-ext004-messaging] +path = 'contracts/dao/extensions/aibtc-ext004-messaging.clar' clarity_version = 2 epoch = 2.5 -[contracts.aibtcdev-messaging] -path = 'contracts/aibtcdev-messaging.clar' +[contracts.aibtc-ext005-payments] +path = 'contracts/dao/extensions/aibtc-ext005-payments.clar' +clarity_version = 2 +epoch = 2.5 + +[contracts.aibtc-ext006-treasury] +path = 'contracts/dao/extensions/aibtc-ext006-treasury.clar' +clarity_version = 2 +epoch = 2.5 + +# dao traits + +[contracts.aibtcdev-dao-v1] +path = 'contracts/dao/traits/aibtcdev-dao-v1.clar' +clarity_version = 2 +epoch = 2.5 + +[contracts.aibtcdev-dao-traits-v1] +path = 'contracts/dao/traits/aibtcdev-dao-traits-v1.clar' +clarity_version = 2 +epoch = 2.5 + +# testing utilities + +[contracts.test-treasury] +path = 'contracts/test/aibtc-treasury.clar' +clarity_version = 2 +epoch = 2.5 + +[contracts.external-proxy] +path = 'contracts/test/proxy.clar' +deployer = 'wallet_1' clarity_version = 2 epoch = 2.5 [contracts.proxy] -path = 'contracts/test-proxy.clar' +path = 'contracts/test/proxy.clar' clarity_version = 2 epoch = 2.5 [contracts.test-proxy] -path = 'contracts/test-proxy.clar' +path = 'contracts/test/proxy.clar' clarity_version = 2 epoch = 2.5 -[repl.analysis] -passes = ['check_checker'] -[repl.analysis.check_checker] -strict = false -trusted_sender = false -trusted_caller = false -callee_filter = false +[contracts.test-token] +path = 'contracts/test/sip010-token.clar' +clarity_version = 2 +epoch = 2.5 diff --git a/contracts/aibtcdev-aibtc.clar b/contracts/aibtcdev-aibtc.clar deleted file mode 100644 index 2cbcce8..0000000 --- a/contracts/aibtcdev-aibtc.clar +++ /dev/null @@ -1,158 +0,0 @@ -;; title: aibtcdev-aibtc -;; version: 0.0.1 -;; summary: Copy of ALEX aBTC contract for use on testnet only. - -;; tokens -;; -(define-fungible-token bridged-btc) - -;; constants -;; -(define-constant ERR-NOT-AUTHORIZED (err u1000)) -(define-constant ONE_8 u100000000) - -;; used for faucet add-on -(define-constant FAUCET-DRIP u10000) ;; 0.0001 BTC -(define-constant FAUCET-DROP u1000000) ;; 0.01 BTC -(define-constant FAUCET-FLOOD u100000000) ;; 1 BTC - -;; data vars -;; -(define-data-var contract-owner principal tx-sender) -(define-data-var token-name (string-ascii 32) "aiBTC") -(define-data-var token-symbol (string-ascii 10) "aiBTC") -(define-data-var token-uri (optional (string-utf8 256)) (some u"https://cdn.alexlab.co/metadata/token-abtc.json")) -(define-data-var token-decimals uint u8) - -;; data maps -;; -(define-map approved-contracts principal bool) - -;; read only functions -;; - -(define-read-only (get-contract-owner) - (ok (var-get contract-owner))) - -(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 (var-get token-decimals))) - -(define-read-only (get-balance (who principal)) - (ok (ft-get-balance bridged-btc who))) - -(define-read-only (get-total-supply) - (ok (ft-get-supply bridged-btc))) - -(define-read-only (get-token-uri) - (ok (var-get token-uri))) - -(define-read-only (fixed-to-decimals (amount uint)) - (/ (* amount (pow-decimals)) ONE_8)) - -(define-private (decimals-to-fixed (amount uint)) - (/ (* amount ONE_8) (pow-decimals))) - -(define-read-only (get-total-supply-fixed) - (ok (decimals-to-fixed (unwrap-panic (get-total-supply))))) - -(define-read-only (get-balance-fixed (account principal)) - (ok (decimals-to-fixed (unwrap-panic (get-balance account))))) - -;; public functions -;; -(define-public (set-contract-owner (owner principal)) - (begin - (try! (check-is-owner)) - (ok (var-set contract-owner owner)))) - -(define-public (set-name (new-name (string-ascii 32))) - (begin - (try! (check-is-owner)) - (ok (var-set token-name new-name)))) - -(define-public (set-symbol (new-symbol (string-ascii 10))) - (begin - (try! (check-is-owner)) - (ok (var-set token-symbol new-symbol)))) - -(define-public (set-decimals (new-decimals uint)) - (begin - (try! (check-is-owner)) - (ok (var-set token-decimals new-decimals)))) - -(define-public (set-token-uri (new-uri (optional (string-utf8 256)))) - (begin - (try! (check-is-owner)) - (ok (var-set token-uri new-uri)))) - -(define-public (add-approved-contract (new-approved-contract principal)) - (begin - (try! (check-is-owner)) - (ok (map-set approved-contracts new-approved-contract true)))) - -(define-public (set-approved-contract (owner principal) (approved bool)) - (begin - (try! (check-is-owner)) - (ok (map-set approved-contracts owner approved)))) - -(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34)))) - (begin - (asserts! (is-eq sender tx-sender) ERR-NOT-AUTHORIZED) - (try! (ft-transfer? bridged-btc amount sender recipient)) - (match memo to-print (print to-print) 0x) - (ok true))) - -(define-public (mint (amount uint) (recipient principal)) - (begin - (asserts! (or (is-ok (check-is-approved)) (is-ok (check-is-owner))) ERR-NOT-AUTHORIZED) - (ft-mint? bridged-btc amount recipient))) - -(define-public (burn (amount uint) (sender principal)) - (begin - (asserts! (or (is-ok (check-is-approved)) (is-ok (check-is-owner))) ERR-NOT-AUTHORIZED) - (ft-burn? bridged-btc amount sender))) - -(define-public (transfer-fixed (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34)))) - (transfer (fixed-to-decimals amount) sender recipient memo)) - -(define-public (mint-fixed (amount uint) (recipient principal)) - (mint (fixed-to-decimals amount) recipient)) - -(define-public (burn-fixed (amount uint) (sender principal)) - (burn (fixed-to-decimals amount) sender)) - -(define-public (burn-fixed-many (senders (list 200 {amount: uint, sender: principal}))) - (begin - (asserts! (or (is-ok (check-is-approved)) (is-ok (check-is-owner))) ERR-NOT-AUTHORIZED) - (ok (map burn-fixed-many-iter senders)))) - -;; adding in some faucet functions for testnet testing -(define-public (faucet-drip (recipient principal)) - (ft-mint? bridged-btc FAUCET-DRIP recipient)) - -(define-public (faucet-drop (recipient principal)) - (ft-mint? bridged-btc FAUCET-DROP recipient)) - -(define-public (faucet-flood (recipient principal)) - (ft-mint? bridged-btc FAUCET-FLOOD recipient)) - - -;; private functions -;; -(define-private (check-is-owner) - (ok (asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-AUTHORIZED))) - -(define-private (check-is-approved) - (ok (asserts! (default-to false (map-get? approved-contracts tx-sender)) ERR-NOT-AUTHORIZED))) - -(define-private (pow-decimals) - (pow u10 (unwrap-panic (get-decimals)))) - -(define-private (burn-fixed-many-iter (item {amount: uint, sender: principal})) - (burn-fixed (get amount item) (get sender item))) diff --git a/contracts/aibtcdev-messaging.clar b/contracts/aibtcdev-messaging.clar deleted file mode 100644 index cffc3fe..0000000 --- a/contracts/aibtcdev-messaging.clar +++ /dev/null @@ -1,24 +0,0 @@ - -;; title: aibtcdev-messaging -;; version: 1.0 -;; summary: A simple messaging contract agents can use. -;; description: Send an on-chain message to anyone listening to this contract. - -;; constants -;; -(define-constant INPUT_ERROR (err u400)) - -;; public functions - -(define-public (send (message (string-ascii 1048576))) - (begin - (asserts! (> (len message) u0) INPUT_ERROR) - (print { - caller: contract-caller, - height: block-height, - sender: tx-sender - }) - (print message) - (ok true) - ) -) diff --git a/contracts/aibtcdev-traits-v1.clar b/contracts/aibtcdev-traits-v1.clar deleted file mode 100644 index 4b7273e..0000000 --- a/contracts/aibtcdev-traits-v1.clar +++ /dev/null @@ -1,15 +0,0 @@ -(define-trait aibtcdev-resource-mgmt-v1 - ( - (set-payment-address (principal principal) (response bool uint)) - (add-resource ((string-utf8 50) (string-utf8 255) uint) (response uint uint)) - (toggle-resource (uint) (response bool uint)) - (toggle-resource-by-name ((string-utf8 50)) (response bool uint)) - ) -) - -(define-trait aibtcdev-invoice-v1 - ( - (pay-invoice (uint (optional (buff 34))) (response uint uint)) - (pay-invoice-by-resource-name ((string-utf8 50) (optional (buff 34))) (response uint uint)) - ) -) diff --git a/contracts/dao/aibtcdev-base-dao.clar b/contracts/dao/aibtcdev-base-dao.clar new file mode 100644 index 0000000..9c1aaba --- /dev/null +++ b/contracts/dao/aibtcdev-base-dao.clar @@ -0,0 +1,136 @@ +;; title: aibtcdev-dao +;; version: 1.0.0 +;; summary: An ExecutorDAO implementation for aibtcdev + +;; traits +;; + +(impl-trait .aibtcdev-dao-v1.aibtcdev-base-dao) +(use-trait proposal-trait .aibtcdev-dao-traits-v1.proposal) +(use-trait extension-trait .aibtcdev-dao-traits-v1.extension) + +;; constants +;; + +(define-constant ERR_UNAUTHORIZED (err u900)) +(define-constant ERR_ALREADY_EXECUTED (err u901)) +(define-constant ERR_INVALID_EXTENSION (err u902)) +(define-constant ERR_NO_EMPTY_LISTS (err u903)) + +;; data vars +;; + +;; used for initial construction, set to contract itself after +(define-data-var executive principal tx-sender) + +;; data maps +;; + +;; tracks block height of executed proposals +(define-map ExecutedProposals principal uint) +;; tracks enabled status of extensions +(define-map Extensions principal bool) + +;; public functions +;; + +;; initial construction of the DAO +(define-public (construct (proposal )) + (let + ((sender tx-sender)) + (asserts! (is-eq sender (var-get executive)) ERR_UNAUTHORIZED) + (var-set executive (as-contract tx-sender)) + (as-contract (execute proposal sender)) + ) +) + +;; execute Clarity code in a proposal +(define-public (execute (proposal ) (sender principal)) + (begin + (try! (is-self-or-extension)) + (asserts! (map-insert ExecutedProposals (contract-of proposal) block-height) ERR_ALREADY_EXECUTED) + (print { + notification: "execute", + payload: { + proposal: proposal, + sender: sender, + } + }) + (as-contract (contract-call? proposal execute sender)) + ) +) + +;; add an extension or update the status of an existing one +(define-public (set-extension (extension principal) (enabled bool)) + (begin + (try! (is-self-or-extension)) + (print { + notification: "extension", + payload: { + enabled: enabled, + extension: extension, + } + }) + (ok (map-set Extensions extension enabled)) + ) +) + +;; add multiple extensions or update the status of existing ones +(define-public (set-extensions (extensionList (list 200 {extension: principal, enabled: bool}))) + (begin + (try! (is-self-or-extension)) + (asserts! (>= (len extensionList) u0) ERR_NO_EMPTY_LISTS) + (ok (map set-extensions-iter extensionList)) + ) +) + +;; request a callback from an extension +(define-public (request-extension-callback (extension ) (memo (buff 34))) + (let + ((sender tx-sender)) + (asserts! (is-extension contract-caller) ERR_INVALID_EXTENSION) + (asserts! (is-eq contract-caller (contract-of extension)) ERR_INVALID_EXTENSION) + (print { + notification: "request-extension-callback", + payload: { + extension: extension, + memo: memo, + sender: sender, + } + }) + (as-contract (contract-call? extension callback sender memo)) + ) +) + +;; read only functions +;; + +(define-read-only (is-extension (extension principal)) + (default-to false (map-get? Extensions extension)) +) + +(define-read-only (executed-at (proposal )) + (map-get? ExecutedProposals (contract-of proposal)) +) + +;; private functions +;; + +;; authorization check +(define-private (is-self-or-extension) + (ok (asserts! (or (is-eq tx-sender (as-contract tx-sender)) (is-extension contract-caller)) ERR_UNAUTHORIZED)) +) + +;; set-extensions helper function +(define-private (set-extensions-iter (item {extension: principal, enabled: bool})) + (begin + (print { + notification: "extension", + payload: { + enabled: (get enabled item), + extension: (get extension item), + } + }) + (map-set Extensions (get extension item) (get enabled item)) + ) +) diff --git a/contracts/dao/extensions/aibtc-ext001-actions.clar b/contracts/dao/extensions/aibtc-ext001-actions.clar new file mode 100644 index 0000000..0587cf3 --- /dev/null +++ b/contracts/dao/extensions/aibtc-ext001-actions.clar @@ -0,0 +1,339 @@ +;; title: aibtcdev-actions +;; version: 1.0.0 +;; summary: An extension that manages voting on predefined actions using a SIP-010 Stacks token. +;; description: This contract allows voting on specific extension actions with a lower threshold than direct-execute. + +;; traits +;; +(impl-trait .aibtcdev-dao-traits-v1.extension) + +(use-trait ft-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) +(use-trait treasury-trait .aibtcdev-dao-traits-v1.treasury) +(use-trait messaging-trait .aibtcdev-dao-traits-v1.messaging) +(use-trait resources-trait .aibtcdev-dao-traits-v1.resources) + +;; constants +;; +(define-constant SELF (as-contract tx-sender)) +(define-constant VOTING_PERIOD u144) ;; 144 Bitcoin blocks, ~1 day +(define-constant VOTING_QUORUM u66) ;; 66% of liquid supply (total supply - treasury) + +(define-constant VALID_ACTIONS (list + "send-message" + "add-resource" + "batch-messages" + "batch-resources" + "allow-asset" + "delegate-stx" + "set-account-holder" + "set-withdrawal-period" + "set-withdrawal-amount" + "toggle-resource" + "set-payment-address" +)) + +;; error messages - authorization +(define-constant ERR_UNAUTHORIZED (err u1000)) +(define-constant ERR_NOT_DAO_OR_EXTENSION (err u1001)) + +;; error messages - initialization +(define-constant ERR_NOT_INITIALIZED (err u1100)) +(define-constant ERR_ALREADY_INITIALIZED (err u1101)) + +;; error messages - treasury +(define-constant ERR_TREASURY_MUST_BE_CONTRACT (err u1200)) +(define-constant ERR_TREASURY_CANNOT_BE_SELF (err u1201)) +(define-constant ERR_TREASURY_ALREADY_SET (err u1202)) +(define-constant ERR_TREASURY_MISMATCH (err u1203)) +(define-constant ERR_TREASURY_NOT_INITIALIZED (err u1204)) + +;; error messages - voting token +(define-constant ERR_TOKEN_MUST_BE_CONTRACT (err u1300)) +(define-constant ERR_TOKEN_NOT_INITIALIZED (err u1301)) +(define-constant ERR_TOKEN_MISMATCH (err u1302)) +(define-constant ERR_INSUFFICIENT_BALANCE (err u1303)) + +;; error messages - proposals +(define-constant ERR_PROPOSAL_NOT_FOUND (err u1400)) +(define-constant ERR_PROPOSAL_ALREADY_EXECUTED (err u1401)) +(define-constant ERR_PROPOSAL_STILL_ACTIVE (err u1402)) +(define-constant ERR_SAVING_PROPOSAL (err u1403)) +(define-constant ERR_PROPOSAL_ALREADY_CONCLUDED (err u1404)) + +;; error messages - voting +(define-constant ERR_VOTE_TOO_SOON (err u1500)) +(define-constant ERR_VOTE_TOO_LATE (err u1501)) +(define-constant ERR_ALREADY_VOTED (err u1502)) +(define-constant ERR_ZERO_VOTING_POWER (err u1503)) +(define-constant ERR_QUORUM_NOT_REACHED (err u1504)) + +;; error messages - actions +(define-constant ERR_INVALID_ACTION (err u1600)) +(define-constant ERR_INVALID_PARAMETERS (err u1601)) + +;; data vars +;; +(define-data-var protocolTreasury principal SELF) ;; the treasury contract for protocol funds +(define-data-var votingToken principal SELF) ;; the FT contract used for voting + +;; data maps +;; +(define-map Proposals + uint ;; proposal id + { + action: (string-ascii 64), ;; action name + parameters: (list 10 (string-utf8 256)), ;; action parameters + createdAt: uint, ;; block height + caller: principal, ;; contract caller + creator: principal, ;; proposal creator (tx-sender) + startBlock: uint, ;; block height + endBlock: uint, ;; block height + votesFor: uint, ;; total votes for + votesAgainst: uint, ;; total votes against + concluded: bool, ;; has the proposal concluded + passed: bool, ;; did the proposal pass + } +) + +(define-map VotingRecords + { + proposalId: uint, ;; proposal id + voter: principal ;; voter address + } + uint ;; total votes +) + +(define-data-var proposalCount uint u0) + +;; public functions +;; + +(define-public (callback (sender principal) (memo (buff 34))) + (ok true) +) + +(define-public (set-protocol-treasury (treasury )) + (let + ( + (treasuryContract (contract-of treasury)) + ) + (try! (is-dao-or-extension)) + ;; treasury must be a contract + (asserts! (not (is-standard treasuryContract)) ERR_TREASURY_MUST_BE_CONTRACT) + ;; treasury must not be already set + (asserts! (is-eq (var-get protocolTreasury) SELF) ERR_TREASURY_NOT_INITIALIZED) + ;; treasury cannot be the voting contract + (asserts! (not (is-eq treasuryContract SELF)) ERR_TREASURY_CANNOT_BE_SELF) + (print { + notification: "set-protocol-treasury", + payload: { + treasury: treasuryContract + } + }) + (ok (var-set protocolTreasury treasuryContract)) + ) +) + +(define-public (set-voting-token (token )) + (let + ( + (tokenContract (contract-of token)) + ) + (try! (is-dao-or-extension)) + ;; token must be a contract + (asserts! (not (is-standard tokenContract)) ERR_TOKEN_MUST_BE_CONTRACT) + ;; token must not be already set + (asserts! (is-eq (var-get votingToken) SELF) ERR_TOKEN_NOT_INITIALIZED) + (print { + notification: "set-voting-token", + payload: { + token: tokenContract + } + }) + (ok (var-set votingToken tokenContract)) + ) +) + +(define-public (propose-action (action (string-ascii 64)) (parameters (list 10 (string-utf8 256))) (token )) + (let + ( + (tokenContract (contract-of token)) + (newId (+ (var-get proposalCount) u1)) + ) + ;; required variables must be set + (asserts! (is-initialized) ERR_NOT_INITIALIZED) + ;; token matches set voting token + (asserts! (is-eq tokenContract (var-get votingToken)) ERR_TOKEN_MISMATCH) + ;; caller has the required balance + (asserts! (> (try! (contract-call? token get-balance tx-sender)) u0) ERR_INSUFFICIENT_BALANCE) + ;; print proposal creation event + (print { + notification: "propose-action", + payload: { + proposalId: newId, + action: action, + parameters: parameters, + creator: tx-sender, + startBlock: burn-block-height, + endBlock: (+ burn-block-height VOTING_PERIOD) + } + }) + ;; create the proposal + (asserts! (map-insert Proposals newId { + action: action, + parameters: parameters, + createdAt: burn-block-height, + caller: contract-caller, + creator: tx-sender, + startBlock: burn-block-height, + endBlock: (+ burn-block-height VOTING_PERIOD), + votesFor: u0, + votesAgainst: u0, + concluded: false, + passed: false, + }) ERR_SAVING_PROPOSAL) + ;; increment proposal count + (ok (var-set proposalCount newId)) + ) +) + +(define-public (vote-on-proposal (proposalId uint) (token ) (vote bool)) + (let + ( + (tokenContract (contract-of token)) + (senderBalance (try! (contract-call? token get-balance tx-sender))) + ) + ;; required variables must be set + (asserts! (is-initialized) ERR_NOT_INITIALIZED) + ;; token matches set voting token + (asserts! (is-eq tokenContract (var-get votingToken)) ERR_TOKEN_MISMATCH) + ;; caller has the required balance + (asserts! (> senderBalance u0) ERR_INSUFFICIENT_BALANCE) + ;; get proposal record + (let + ( + (proposalRecord (unwrap! (map-get? Proposals proposalId) ERR_PROPOSAL_NOT_FOUND)) + ) + ;; proposal is still active + (asserts! (>= burn-block-height (get startBlock proposalRecord)) ERR_VOTE_TOO_SOON) + (asserts! (< burn-block-height (get endBlock proposalRecord)) ERR_VOTE_TOO_LATE) + ;; proposal not already concluded + (asserts! (not (get concluded proposalRecord)) ERR_PROPOSAL_ALREADY_CONCLUDED) + ;; vote not already cast + (asserts! (is-none (map-get? VotingRecords {proposalId: proposalId, voter: tx-sender})) ERR_ALREADY_VOTED) + ;; print vote event + (print { + notification: "vote-on-proposal", + payload: { + proposalId: proposalId, + voter: tx-sender, + amount: senderBalance + } + }) + ;; update the proposal record + (map-set Proposals proposalId + (if vote + (merge proposalRecord {votesFor: (+ (get votesFor proposalRecord) senderBalance)}) + (merge proposalRecord {votesAgainst: (+ (get votesAgainst proposalRecord) senderBalance)}) + ) + ) + ;; record the vote for the sender + (ok (map-set VotingRecords {proposalId: proposalId, voter: tx-sender} senderBalance)) + ) + ) +) + +(define-public (conclude-proposal (proposalId uint) (treasury ) (token )) + (let + ( + (proposalRecord (unwrap! (map-get? Proposals proposalId) ERR_PROPOSAL_NOT_FOUND)) + (tokenContract (contract-of token)) + (tokenTotalSupply (try! (contract-call? token get-total-supply))) + (treasuryContract (contract-of treasury)) + (treasuryBalance (try! (contract-call? token get-balance treasuryContract))) + (votePassed (> (get votesFor proposalRecord) (* tokenTotalSupply (- u100 treasuryBalance) VOTING_QUORUM))) + ) + ;; required variables must be set + (asserts! (is-initialized) ERR_NOT_INITIALIZED) + ;; verify treasury matches protocol treasury + (asserts! (is-eq treasuryContract (var-get protocolTreasury)) ERR_TREASURY_MISMATCH) + ;; proposal past end block height + (asserts! (>= burn-block-height (get endBlock proposalRecord)) ERR_PROPOSAL_STILL_ACTIVE) + ;; proposal not already concluded + (asserts! (not (get concluded proposalRecord)) ERR_PROPOSAL_ALREADY_CONCLUDED) + ;; print conclusion event + (print { + notification: "conclude-proposal", + payload: { + proposalId: proposalId, + passed: votePassed + } + }) + ;; update the proposal record + (map-set Proposals proposalId + (merge proposalRecord { + concluded: true, + passed: votePassed + }) + ) + ;; execute the action only if it passed + ;; (and votePassed (try! (execute-action proposalRecord))) + ;; return the result + (ok votePassed) + ) +) + +;; read only functions +;; + +(define-read-only (get-protocol-treasury) + (if (is-eq (var-get protocolTreasury) SELF) + none + (some (var-get protocolTreasury)) + ) +) + +(define-read-only (get-voting-token) + (if (is-eq (var-get votingToken) SELF) + none + (some (var-get votingToken)) + ) +) + +(define-read-only (get-proposal (proposalId uint)) + (map-get? Proposals proposalId) +) + +(define-read-only (get-total-votes (proposalId uint) (voter principal)) + (default-to u0 (map-get? VotingRecords {proposalId: proposalId, voter: voter})) +) + +(define-read-only (is-initialized) + ;; check if the required variables are set + (not (or + (is-eq (var-get votingToken) SELF) + (is-eq (var-get protocolTreasury) SELF) + )) +) + +(define-read-only (get-voting-period) + VOTING_PERIOD +) + +(define-read-only (get-voting-quorum) + VOTING_QUORUM +) + +(define-read-only (get-total-proposals) + (var-get proposalCount) +) + +;; private functions +;; + +(define-private (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .aibtcdev-dao) + (contract-call? .aibtcdev-base-dao is-extension contract-caller)) ERR_NOT_DAO_OR_EXTENSION + )) +) + diff --git a/contracts/aibtcdev-bank-account.clar b/contracts/dao/extensions/aibtc-ext002-bank-account.clar similarity index 60% rename from contracts/aibtcdev-bank-account.clar rename to contracts/dao/extensions/aibtc-ext002-bank-account.clar index 1b19434..48745b6 100644 --- a/contracts/aibtcdev-bank-account.clar +++ b/contracts/dao/extensions/aibtc-ext002-bank-account.clar @@ -1,14 +1,20 @@ ;; title: aibtcdev-bank-account ;; version: 1.0.0 -;; summary: A contract that allows specified principals to withdraw STX from the contract with given rules. +;; summary: An extension that allows a principal to withdraw STX from the contract with given rules. + +;; traits +;; +(impl-trait .aibtcdev-dao-traits-v1.extension) +(impl-trait .aibtcdev-dao-traits-v1.bank-account) ;; constants ;; -(define-constant DEPLOYER tx-sender) (define-constant SELF (as-contract tx-sender)) -(define-constant ERR_INVALID (err u1000)) -(define-constant ERR_UNAUTHORIZED (err u1001)) -(define-constant ERR_TOO_SOON (err u1002)) +(define-constant DEPLOYED_AT burn-block-height) +(define-constant ERR_INVALID (err u2000)) +(define-constant ERR_UNAUTHORIZED (err u2001)) +(define-constant ERR_TOO_SOON (err u2002)) +(define-constant ERR_INVALID_AMOUNT (err u2003)) ;; data vars @@ -22,9 +28,13 @@ ;; public functions ;; +(define-public (callback (sender principal) (memo (buff 34))) + (ok true) +) + (define-public (set-account-holder (new principal)) (begin - (try! (is-deployer)) + (try! (is-dao-or-extension)) (asserts! (not (is-eq (var-get accountHolder) new)) ERR_INVALID) (ok (var-set accountHolder new)) ) @@ -32,7 +42,7 @@ (define-public (set-withdrawal-period (period uint)) (begin - (try! (is-deployer)) + (try! (is-dao-or-extension)) (asserts! (> period u0) ERR_INVALID) (ok (var-set withdrawalPeriod period)) ) @@ -40,7 +50,7 @@ (define-public (set-withdrawal-amount (amount uint)) (begin - (try! (is-deployer)) + (try! (is-dao-or-extension)) (asserts! (> amount u0) ERR_INVALID) (ok (var-set withdrawalAmount amount)) ) @@ -48,14 +58,15 @@ (define-public (override-last-withdrawal-block (block uint)) (begin - (try! (is-deployer)) - (asserts! (> block u0) ERR_INVALID) + (try! (is-dao-or-extension)) + (asserts! (> block DEPLOYED_AT) ERR_INVALID) (ok (var-set lastWithdrawalBlock block)) ) ) (define-public (deposit-stx (amount uint)) (begin + (asserts! (> amount u0) ERR_INVALID_AMOUNT) (print { notification: "deposit-stx", payload: { @@ -73,9 +84,9 @@ ;; verify user is enabled in the map (try! (is-account-holder)) ;; verify user is not withdrawing too soon - (asserts! (>= block-height (+ (var-get lastWithdrawalBlock) (var-get withdrawalPeriod))) ERR_TOO_SOON) + (asserts! (>= burn-block-height (+ (var-get lastWithdrawalBlock) (var-get withdrawalPeriod))) ERR_TOO_SOON) ;; update last withdrawal block - (var-set lastWithdrawalBlock block-height) + (var-set lastWithdrawalBlock burn-block-height) ;; print notification and transfer STX (print { notification: "withdraw-stx", @@ -89,13 +100,24 @@ ) ) - ;; read only functions ;; +(define-read-only (get-deployed-block) + DEPLOYED_AT +) + (define-read-only (get-account-balance) (stx-get-balance SELF) ) +(define-read-only (get-account-holder) + (var-get accountHolder) +) + +(define-read-only (get-last-withdrawal-block) + (var-get lastWithdrawalBlock) +) + (define-read-only (get-withdrawal-period) (var-get withdrawalPeriod) ) @@ -104,30 +126,33 @@ (var-get withdrawalAmount) ) -(define-read-only (get-last-withdrawal-block) - (var-get lastWithdrawalBlock) -) - -(define-read-only (get-all-vars) +(define-read-only (get-account-terms) { - withdrawalPeriod: (var-get withdrawalPeriod), - withdrawalAmount: (var-get withdrawalAmount), - lastWithdrawalBlock: (var-get lastWithdrawalBlock) + accountBalance: (get-account-balance), + accountHolder: (get-account-holder), + contractName: SELF, + deployedAt: (get-deployed-block), + lastWithdrawalBlock: (get-last-withdrawal-block), + withdrawalAmount: (get-withdrawal-amount), + withdrawalPeriod: (get-withdrawal-period), } ) -(define-read-only (get-standard-caller) - (let ((d (unwrap-panic (principal-destruct? contract-caller)))) - (unwrap-panic (principal-construct? (get version d) (get hash-bytes d))) - ) -) - ;; private functions ;; -(define-private (is-deployer) - (ok (asserts! (is-eq DEPLOYER (get-standard-caller)) ERR_UNAUTHORIZED)) + +(define-private (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .aibtcdev-dao) + (contract-call? .aibtcdev-base-dao is-extension contract-caller)) ERR_UNAUTHORIZED + )) ) (define-private (is-account-holder) (ok (asserts! (is-eq (var-get accountHolder) (get-standard-caller)) ERR_UNAUTHORIZED)) ) + +(define-private (get-standard-caller) + (let ((d (unwrap-panic (principal-destruct? contract-caller)))) + (unwrap-panic (principal-construct? (get version d) (get hash-bytes d))) + ) +) diff --git a/contracts/dao/extensions/aibtc-ext003-direct-execute.clar b/contracts/dao/extensions/aibtc-ext003-direct-execute.clar new file mode 100644 index 0000000..849276d --- /dev/null +++ b/contracts/dao/extensions/aibtc-ext003-direct-execute.clar @@ -0,0 +1,309 @@ +;; title: aibtcdev-direct-execute +;; version: 1.0.0 +;; summary: An extension that manages voting on proposals to execute Clarity code using a SIP-010 Stacks token. +;; description: This contract can make changes to core DAO functionality with a high voting threshold by executing Clarity code in the context of the DAO. + +;; traits +;; +(impl-trait .aibtcdev-dao-traits-v1.extension) +(impl-trait .aibtcdev-dao-traits-v1.direct-execute) + +(use-trait ft-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) +(use-trait proposal-trait .aibtcdev-dao-traits-v1.proposal) +(use-trait treasury-trait .aibtcdev-dao-traits-v1.treasury) + + +;; constants +;; + +(define-constant SELF (as-contract tx-sender)) +(define-constant VOTING_PERIOD u144) ;; 144 Bitcoin blocks, ~1 day +(define-constant VOTING_QUORUM u95) ;; 95% of liquid supply (total supply - treasury) + +;; error messages - authorization +(define-constant ERR_UNAUTHORIZED (err u3000)) +(define-constant ERR_NOT_DAO_OR_EXTENSION (err u3001)) + +;; error messages - initialization +(define-constant ERR_NOT_INITIALIZED (err u3100)) +(define-constant ERR_ALREADY_INITIALIZED (err u3101)) + +;; error messages - treasury +(define-constant ERR_TREASURY_MUST_BE_CONTRACT (err u3200)) +(define-constant ERR_TREASURY_CANNOT_BE_SELF (err u3201)) +(define-constant ERR_TREASURY_ALREADY_SET (err u3202)) +(define-constant ERR_TREASURY_MISMATCH (err u3203)) + +;; error messages - voting token +(define-constant ERR_TOKEN_MUST_BE_CONTRACT (err u3300)) +(define-constant ERR_TOKEN_NOT_INITIALIZED (err u3301)) +(define-constant ERR_TOKEN_MISMATCH (err u3302)) +(define-constant ERR_INSUFFICIENT_BALANCE (err u3303)) + +;; error messages - proposals +(define-constant ERR_PROPOSAL_NOT_FOUND (err u3400)) +(define-constant ERR_PROPOSAL_ALREADY_EXECUTED (err u3401)) +(define-constant ERR_PROPOSAL_STILL_ACTIVE (err u3402)) +(define-constant ERR_SAVING_PROPOSAL (err u3403)) +(define-constant ERR_PROPOSAL_ALREADY_CONCLUDED (err u3404)) + +;; error messages - voting +(define-constant ERR_VOTE_TOO_SOON (err u3500)) +(define-constant ERR_VOTE_TOO_LATE (err u3501)) +(define-constant ERR_ALREADY_VOTED (err u3502)) +(define-constant ERR_ZERO_VOTING_POWER (err u3503)) +(define-constant ERR_QUORUM_NOT_REACHED (err u3504)) + +;; data vars +;; +(define-data-var protocolTreasury principal SELF) ;; the treasury contract for protocol funds +(define-data-var votingToken principal SELF) ;; the FT contract used for voting + +;; data maps +;; +(define-map Proposals + principal ;; proposal contract + { + createdAt: uint, ;; block height + caller: principal, ;; contract caller + creator: principal, ;; proposal creator (tx-sender) + startBlock: uint, ;; block height + endBlock: uint, ;; block height + votesFor: uint, ;; total votes for + votesAgainst: uint, ;; total votes against + concluded: bool, ;; has the proposal concluded + passed: bool, ;; did the proposal pass + } +) + +(define-map VotingRecords + { + proposal: principal, ;; proposal contract + voter: principal ;; voter address + } + uint ;; total votes +) + +;; public functions +;; + +(define-public (callback (sender principal) (memo (buff 34))) + (ok true) +) + +(define-public (set-protocol-treasury (treasury )) + (let + ( + (treasuryContract (contract-of treasury)) + ) + (try! (is-dao-or-extension)) + ;; treasury must be a contract + (asserts! (not (is-standard treasuryContract)) ERR_TREASURY_MUST_BE_CONTRACT) + ;; treasury cannot be the voting contract + (asserts! (not (is-eq treasuryContract SELF)) ERR_TREASURY_CANNOT_BE_SELF) + ;; treasury cannot be the same value + (asserts! (not (is-eq treasuryContract (var-get protocolTreasury))) ERR_TREASURY_ALREADY_SET) + (print { + notification: "set-protocol-treasury", + payload: { + treasury: treasuryContract + } + }) + (ok (var-set protocolTreasury treasuryContract)) + ) +) + +(define-public (set-voting-token (token )) + (let + ( + (tokenContract (contract-of token)) + ) + (try! (is-dao-or-extension)) + ;; token must be a contract + (asserts! (not (is-standard tokenContract)) ERR_TOKEN_MUST_BE_CONTRACT) + (asserts! (is-eq (var-get votingToken) SELF) ERR_TOKEN_NOT_INITIALIZED) + (asserts! (is-eq (var-get votingToken) tokenContract) ERR_TOKEN_MISMATCH) + (print { + notification: "set-voting-token", + payload: { + token: tokenContract + } + }) + (ok (var-set votingToken tokenContract)) + ) +) + +(define-public (create-proposal (proposal ) (token )) + (let + ( + (proposalContract (contract-of proposal)) + (tokenContract (contract-of token)) + ) + ;; required variables must be set + (asserts! (is-initialized) ERR_NOT_INITIALIZED) + ;; token matches set voting token + (asserts! (is-eq tokenContract (var-get votingToken)) ERR_TOKEN_MISMATCH) + ;; caller has the required balance + (asserts! (> (try! (contract-call? token get-balance tx-sender)) u0) ERR_INSUFFICIENT_BALANCE) + ;; proposal was not already executed + (asserts! (is-none (contract-call? .aibtcdev-base-dao executed-at proposal)) ERR_PROPOSAL_ALREADY_EXECUTED) + ;; print proposal creation event + (print { + notification: "create-proposal", + payload: { + proposal: proposalContract, + creator: tx-sender, + startBlock: burn-block-height, + endBlock: (+ burn-block-height VOTING_PERIOD) + } + }) + ;; create the proposal + (ok (asserts! (map-insert Proposals proposalContract { + createdAt: burn-block-height, + caller: contract-caller, + creator: tx-sender, + startBlock: burn-block-height, + endBlock: (+ burn-block-height VOTING_PERIOD), + votesFor: u0, + votesAgainst: u0, + concluded: false, + passed: false, + }) ERR_SAVING_PROPOSAL)) +)) + +(define-public (vote-on-proposal (proposal ) (token ) (vote bool)) + (let + ( + (proposalContract (contract-of proposal)) + (proposalRecord (unwrap! (map-get? Proposals proposalContract) ERR_PROPOSAL_NOT_FOUND)) + (tokenContract (contract-of token)) + (senderBalance (try! (contract-call? token get-balance tx-sender))) + ) + ;; required variables must be set + (asserts! (is-initialized) ERR_NOT_INITIALIZED) + ;; token matches set voting token + (asserts! (is-eq tokenContract (var-get votingToken)) ERR_TOKEN_MISMATCH) + ;; caller has the required balance + (asserts! (> senderBalance u0) ERR_ZERO_VOTING_POWER) + ;; proposal was not already executed + (asserts! (is-none (contract-call? .aibtcdev-base-dao executed-at proposal)) ERR_PROPOSAL_ALREADY_EXECUTED) + ;; proposal is still active + (asserts! (>= burn-block-height (get startBlock proposalRecord)) ERR_VOTE_TOO_SOON) + (asserts! (< burn-block-height (get endBlock proposalRecord)) ERR_VOTE_TOO_LATE) + ;; proposal not already concluded + (asserts! (not (get concluded proposalRecord)) ERR_PROPOSAL_ALREADY_CONCLUDED) + ;; vote not already cast + (asserts! (is-none (map-get? VotingRecords {proposal: proposalContract, voter: tx-sender})) ERR_ALREADY_VOTED) + ;; print vote event + (print { + notification: "vote-on-proposal", + payload: { + proposal: proposalContract, + voter: tx-sender, + amount: senderBalance + } + }) + ;; update the proposal record + (map-set Proposals proposalContract + (if vote + (merge proposalRecord {votesFor: (+ (get votesFor proposalRecord) senderBalance)}) + (merge proposalRecord {votesAgainst: (+ (get votesAgainst proposalRecord) senderBalance)}) + ) + ) + ;; record the vote for the sender + (ok (map-set VotingRecords {proposal: proposalContract, voter: tx-sender} senderBalance)) + ) +) + +(define-public (conclude-proposal (proposal ) (treasury ) (token )) + (let + ( + (proposalContract (contract-of proposal)) + (proposalRecord (unwrap! (map-get? Proposals proposalContract) ERR_PROPOSAL_NOT_FOUND)) + (tokenContract (contract-of token)) + (tokenTotalSupply (try! (contract-call? token get-total-supply))) + (treasuryContract (contract-of treasury)) + (treasuryBalance (try! (contract-call? token get-balance treasuryContract))) + (votePassed (> (get votesFor proposalRecord) (* tokenTotalSupply (- u100 treasuryBalance) VOTING_QUORUM))) + ) + ;; required variables must be set + (asserts! (is-initialized) ERR_NOT_INITIALIZED) + ;; verify treasury matches protocol treasury + (asserts! (is-eq treasuryContract (var-get protocolTreasury)) ERR_TREASURY_MISMATCH) + ;; proposal was not already executed + (asserts! (is-none (contract-call? .aibtcdev-base-dao executed-at proposal)) ERR_PROPOSAL_ALREADY_EXECUTED) + ;; proposal past end block height + (asserts! (>= burn-block-height (get endBlock proposalRecord)) ERR_PROPOSAL_STILL_ACTIVE) + ;; proposal not already concluded + (asserts! (not (get concluded proposalRecord)) ERR_PROPOSAL_ALREADY_CONCLUDED) + ;; print conclusion event + (print { + notification: "conclude-proposal", + payload: { + proposal: proposalContract, + passed: votePassed + } + }) + ;; update the proposal record + (map-set Proposals proposalContract + (merge proposalRecord { + concluded: true, + passed: votePassed + }) + ) + ;; execute the proposal only if it passed + (and votePassed (try! (contract-call? .aibtcdev-base-dao execute proposal tx-sender))) + ;; return the result + (ok votePassed) + ) +) + +;; read only functions +;; + +(define-read-only (get-protocol-treasury) + (if (is-eq (var-get protocolTreasury) SELF) + none + (some (var-get protocolTreasury)) + ) +) + +(define-read-only (get-voting-token) + (if (is-eq (var-get votingToken) SELF) + none + (some (var-get votingToken)) + ) +) + +(define-read-only (get-proposal (proposal principal)) + (map-get? Proposals proposal) +) + +(define-read-only (get-total-votes (proposal principal) (voter principal)) + (default-to u0 (map-get? VotingRecords {proposal: proposal, voter: voter})) +) + +(define-read-only (is-initialized) + ;; check if the required variables are set + (not (or + (is-eq (var-get votingToken) SELF) + (is-eq (var-get protocolTreasury) SELF) + )) +) + +(define-read-only (get-voting-period) + VOTING_PERIOD +) + +(define-read-only (get-voting-quorum) + VOTING_QUORUM +) + +;; private functions +;; + +(define-private (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .aibtcdev-dao) + (contract-call? .aibtcdev-base-dao is-extension contract-caller)) ERR_NOT_DAO_OR_EXTENSION + )) +) diff --git a/contracts/dao/extensions/aibtc-ext004-messaging.clar b/contracts/dao/extensions/aibtc-ext004-messaging.clar new file mode 100644 index 0000000..7843a02 --- /dev/null +++ b/contracts/dao/extensions/aibtc-ext004-messaging.clar @@ -0,0 +1,48 @@ +;; title: aibtcdev-messaging +;; version: 1.0.0 +;; summary: An extension to send messages on-chain to anyone listening to this contract. + +;; traits +;; +(impl-trait .aibtcdev-dao-traits-v1.extension) +(impl-trait .aibtcdev-dao-traits-v1.messaging) + +;; constants +;; +(define-constant INPUT_ERROR (err u400)) +(define-constant ERR_UNAUTHORIZED (err u2000)) + +;; public functions + +(define-public (callback (sender principal) (memo (buff 34))) + (ok true) +) + +(define-public (send (msg (string-ascii 1048576)) (isFromDao bool)) + (begin + (and isFromDao (try! (is-dao-or-extension))) + (asserts! (> (len msg) u0) INPUT_ERROR) + ;; print the message as the first event + (print msg) + ;; print the envelope info for the message + (print { + notification: "send", + payload: { + caller: contract-caller, + height: block-height, + isFromDao: isFromDao, + sender: tx-sender, + } + }) + (ok true) + ) +) + +;; private functions +;; + +(define-private (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .aibtcdev-dao) + (contract-call? .aibtcdev-base-dao is-extension contract-caller)) ERR_UNAUTHORIZED + )) +) diff --git a/contracts/aibtcdev-resources-v1.clar b/contracts/dao/extensions/aibtc-ext005-payments.clar similarity index 74% rename from contracts/aibtcdev-resources-v1.clar rename to contracts/dao/extensions/aibtc-ext005-payments.clar index a5113ff..ee8d62c 100644 --- a/contracts/aibtcdev-resources-v1.clar +++ b/contracts/dao/extensions/aibtc-ext005-payments.clar @@ -1,38 +1,37 @@ - -;; title: aibtcdev-resources-v1 -;; version: 0.0.2 -;; summary: HTTP 402 payments powered by Stacks +;; title: aibtcdev-payments +;; version: 1.0.0 +;; summary: An extension that provides payment processing for aibtcdev services. ;; traits ;; -(impl-trait .aibtcdev-traits-v1.aibtcdev-resource-mgmt-v1) -(impl-trait .aibtcdev-traits-v1.aibtcdev-invoice-v1) +(impl-trait .aibtcdev-dao-traits-v1.extension) +(impl-trait .aibtcdev-dao-traits-v1.invoices) +(impl-trait .aibtcdev-dao-traits-v1.resources) ;; constants ;; ;; initially scoped to service provider deploying a contract -(define-constant DEPLOYER contract-caller) (define-constant SELF (as-contract tx-sender)) ;; math helpers (credit: ALEX) (define-constant ONE_8 (pow u10 u8)) ;; errors -(define-constant ERR_UNAUTHORIZED (err u1000)) -(define-constant ERR_INVALID_PARAMS (err u1001)) -(define-constant ERR_NAME_ALREADY_USED (err u1002)) -(define-constant ERR_SAVING_RESOURCE_DATA (err u1003)) -(define-constant ERR_DELETING_RESOURCE_DATA (err u1004)) -(define-constant ERR_RESOURCE_NOT_FOUND (err u1005)) -(define-constant ERR_RESOURCE_NOT_ENABLED (err u1006)) -(define-constant ERR_USER_ALREADY_EXISTS (err u1007)) -(define-constant ERR_SAVING_USER_DATA (err u1008)) -(define-constant ERR_USER_NOT_FOUND (err u1009)) -(define-constant ERR_INVOICE_ALREADY_PAID (err u1010)) -(define-constant ERR_SAVING_INVOICE_DATA (err u1011)) -(define-constant ERR_INVOICE_NOT_FOUND (err u1012)) -(define-constant ERR_RECENT_PAYMENT_NOT_FOUND (err u1013)) +(define-constant ERR_UNAUTHORIZED (err u5000)) +(define-constant ERR_INVALID_PARAMS (err u5001)) +(define-constant ERR_NAME_ALREADY_USED (err u5002)) +(define-constant ERR_SAVING_RESOURCE_DATA (err u5003)) +(define-constant ERR_DELETING_RESOURCE_DATA (err u5004)) +(define-constant ERR_RESOURCE_NOT_FOUND (err u5005)) +(define-constant ERR_RESOURCE_DISABLED (err u5006)) +(define-constant ERR_USER_ALREADY_EXISTS (err u5007)) +(define-constant ERR_SAVING_USER_DATA (err u5008)) +(define-constant ERR_USER_NOT_FOUND (err u5009)) +(define-constant ERR_INVOICE_ALREADY_PAID (err u5010)) +(define-constant ERR_SAVING_INVOICE_DATA (err u5011)) +(define-constant ERR_INVOICE_NOT_FOUND (err u5012)) +(define-constant ERR_RECENT_PAYMENT_NOT_FOUND (err u5013)) ;; data vars ;; @@ -45,8 +44,8 @@ ;; tracking overall contract revenue (define-data-var totalRevenue uint u0) -;; payout address, deployer can set -(define-data-var paymentAddress principal DEPLOYER) +;; dao can update payment address +(define-data-var paymentAddress principal .aibtc-ext002-bank-account) ;; data maps ;; @@ -74,7 +73,7 @@ uint ;; resource index ) -;; tracks resources added by deployer keyed by resource index +;; tracks resources added by dao, keyed by resource index ;; can iterate over full map with resourceCount data-var (define-map ResourceData uint ;; resource index @@ -86,9 +85,7 @@ price: uint, totalSpent: uint, totalUsed: uint, - ;; TODO: for health check, setter would be nice - ;; TODO: expect SIP-018 open timestamp response - ;; url: (optional (string-utf8 255)), + url: (optional (string-utf8 255)), } ) @@ -113,112 +110,28 @@ uint ;; invoice count ) - -;; read only functions +;; public functions ;; -;; returns total registered users -(define-read-only (get-total-users) - (var-get userCount) -) - -;; returns user index for address if known -(define-read-only (get-user-index (user principal)) - (map-get? UserIndexes user) -) - -;; returns user data by user index if known -(define-read-only (get-user-data (index uint)) - (map-get? UserData index) -) - -;; returns user data by address if known -(define-read-only (get-user-data-by-address (user principal)) - (get-user-data (unwrap! (get-user-index user) none)) -) - -;; returns total registered resources -(define-read-only (get-total-resources) - (var-get resourceCount) -) - -;; returns resource index for name if known -(define-read-only (get-resource-index (name (string-utf8 50))) - (map-get? ResourceIndexes name) -) - -;; returns resource data by resource index if known -(define-read-only (get-resource (index uint)) - (map-get? ResourceData index) -) - -;; returns resource data by resource name if known -(define-read-only (get-resource-by-name (name (string-utf8 50))) - (get-resource (unwrap! (get-resource-index name) none)) +(define-public (callback (sender principal) (memo (buff 34))) + (ok true) ) -;; returns total registered invoices -(define-read-only (get-total-invoices) - (var-get invoiceCount) -) - -;; returns invoice data by invoice index if known -(define-read-only (get-invoice (index uint)) - (map-get? InvoiceData index) -) - -;; returns invoice index by user index and resource index if known -(define-read-only (get-recent-payment (resourceIndex uint) (userIndex uint)) - (map-get? RecentPayments { - userIndex: userIndex, - resourceIndex: resourceIndex, - }) -) - -;; returns invoice data by user index and resource index if known -(define-read-only (get-recent-payment-data (resourceIndex uint) (userIndex uint)) - (get-invoice (unwrap! (get-recent-payment resourceIndex userIndex) none)) -) - -;; returns invoice data by user address and resource name if known -(define-read-only (get-recent-payment-data-by-address (name (string-utf8 50)) (user principal)) - (get-recent-payment-data (unwrap! (get-resource-index name) none) (unwrap! (get-user-index user) none)) -) - -;; returns payment address -(define-read-only (get-payment-address) - (some (var-get paymentAddress)) -) - -;; returns total revenue -(define-read-only (get-total-revenue) - (var-get totalRevenue) -) - -;; public functions -;; - ;; sets payment address used for invoices -;; only accessible by deployer or current payment address -(define-public (set-payment-address (oldAddress principal) (newAddress principal)) +(define-public (set-payment-address (newAddress principal)) (begin - ;; check that old address matches current address - (asserts! (is-eq oldAddress (var-get paymentAddress)) ERR_UNAUTHORIZED) - ;; address cannot be the same - (asserts! (not (is-eq oldAddress newAddress)) ERR_UNAUTHORIZED) - ;; check if caller matches deployer or oldAddress - (asserts! (or - (is-eq contract-caller oldAddress) - (try! (is-deployer)) - ) ERR_UNAUTHORIZED) + ;; check if caller is authorized + (try! (is-dao-or-extension)) + ;; check that new address differs from current address + (asserts! (not (is-eq newAddress (var-get paymentAddress))) ERR_UNAUTHORIZED) ;; print details (print { notification: "set-payment-address", payload: { - oldAddress: oldAddress, + contractCaller: contract-caller, + oldAddress: (var-get paymentAddress), newAddress: newAddress, txSender: tx-sender, - contractCaller: contract-caller, } }) ;; set new payment address @@ -227,31 +140,32 @@ ) ;; adds active resource that invoices can be generated for -;; only accessible by deployer -(define-public (add-resource (name (string-utf8 50)) (description (string-utf8 255)) (price uint)) +(define-public (add-resource (name (string-utf8 50)) (description (string-utf8 255)) (price uint) (url (optional (string-utf8 255)))) (let ( (newCount (+ (get-total-resources) u1)) ) - ;; check if caller matches deployer - (try! (is-deployer)) + ;; check if caller is authorized + (try! (is-dao-or-extension)) ;; check all values are provided (asserts! (> (len name) u0) ERR_INVALID_PARAMS) (asserts! (> (len description) u0) ERR_INVALID_PARAMS) (asserts! (> price u0) ERR_INVALID_PARAMS) + (and (is-some url) (asserts! (> (len (unwrap-panic url)) u0) ERR_INVALID_PARAMS)) ;; update ResourceIndexes map, check name is unique (asserts! (map-insert ResourceIndexes name newCount) ERR_NAME_ALREADY_USED) ;; update ResourceData map (asserts! (map-insert ResourceData newCount { - createdAt: block-height, + createdAt: burn-block-height, enabled: true, name: name, description: description, price: price, totalSpent: u0, totalUsed: u0, + url: url, } ) ERR_SAVING_RESOURCE_DATA) ;; increment resourceCount @@ -260,10 +174,10 @@ (print { notification: "add-resource", payload: { - resourceIndex: newCount, + contractCaller: contract-caller, resourceData: (unwrap! (get-resource newCount) ERR_RESOURCE_NOT_FOUND), - txSender: tx-sender, - contractCaller: contract-caller + resourceIndex: newCount, + txSender: tx-sender } }) ;; return new count @@ -272,20 +186,19 @@ ) ;; toggles enabled status for resource -;; only accessible by deployer -(define-public (toggle-resource (index uint)) +(define-public (toggle-resource (resourceIndex uint)) (let ( - (resourceData (unwrap! (get-resource index) ERR_RESOURCE_NOT_FOUND)) + (resourceData (unwrap! (get-resource resourceIndex) ERR_RESOURCE_NOT_FOUND)) (newStatus (not (get enabled resourceData))) ) ;; verify resource > 0 - (asserts! (> index u0) ERR_INVALID_PARAMS) - ;; check if caller matches deployer - (try! (is-deployer)) + (asserts! (> resourceIndex u0) ERR_INVALID_PARAMS) + ;; check if caller is authorized + (try! (is-dao-or-extension)) ;; update ResourceData map (map-set ResourceData - index + resourceIndex (merge resourceData { enabled: newStatus }) @@ -294,8 +207,8 @@ (print { notification: "toggle-resource", payload: { - resourceIndex: index, - resourceData: (unwrap! (get-resource index) ERR_RESOURCE_NOT_FOUND), + resourceIndex: resourceIndex, + resourceData: (unwrap! (get-resource resourceIndex) ERR_RESOURCE_NOT_FOUND), txSender: tx-sender, contractCaller: contract-caller } @@ -306,7 +219,6 @@ ) ;; toggles enabled status for resource by name -;; only accessible by deployer (define-public (toggle-resource-by-name (name (string-utf8 50))) (toggle-resource (unwrap! (get-resource-index name) ERR_RESOURCE_NOT_FOUND)) ) @@ -316,7 +228,7 @@ (let ( (newCount (+ (get-total-invoices) u1)) - (lastAnchoredBlock (- block-height u1)) + (lastAnchoredBlock (- burn-block-height u1)) (resourceData (unwrap! (get-resource resourceIndex) ERR_RESOURCE_NOT_FOUND)) (userIndex (unwrap! (get-or-create-user contract-caller) ERR_USER_NOT_FOUND)) (userData (unwrap! (get-user-data userIndex) ERR_USER_NOT_FOUND)) @@ -324,13 +236,13 @@ ;; check that resourceIndex is > 0 (asserts! (> resourceIndex u0) ERR_INVALID_PARAMS) ;; check that resource is enabled - (asserts! (get enabled resourceData) ERR_RESOURCE_NOT_ENABLED) + (asserts! (get enabled resourceData) ERR_RESOURCE_DISABLED) ;; update InvoiceData map (asserts! (map-insert InvoiceData newCount { amount: (get price resourceData), - createdAt: block-height, + createdAt: burn-block-height, userIndex: userIndex, resourceName: (get name resourceData), resourceIndex: resourceIndex, @@ -368,25 +280,22 @@ (print { notification: "pay-invoice", payload: { - invoiceIndex: newCount, + contractCaller: contract-caller, invoiceData: (unwrap! (get-invoice newCount) ERR_INVOICE_NOT_FOUND), + invoiceIndex: newCount, recentPayment: (unwrap! (get-recent-payment resourceIndex userIndex) ERR_RECENT_PAYMENT_NOT_FOUND), - userIndex: userIndex, - userData: (unwrap! (get-user-data userIndex) ERR_USER_NOT_FOUND), - resourceIndex: resourceIndex, resourceData: (unwrap! (get-resource resourceIndex) ERR_RESOURCE_NOT_FOUND), + resourceIndex: resourceIndex, totalRevenue: (var-get totalRevenue), txSender: tx-sender, - contractCaller: contract-caller + userIndex: userIndex, + userData: (unwrap! (get-user-data userIndex) ERR_USER_NOT_FOUND) } }) ;; make transfer (if (is-some memo) - ;; MAINNET - ;; xBTC SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.token-wbtc - ;; aBTC SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.token-abtc - (try! (contract-call? .aibtcdev-aibtc transfer (get price resourceData) contract-caller (var-get paymentAddress) memo)) - (try! (contract-call? .aibtcdev-aibtc transfer (get price resourceData) contract-caller (var-get paymentAddress) none)) + (try! (stx-transfer-memo? (get price resourceData) contract-caller (var-get paymentAddress) (unwrap-panic memo))) + (try! (stx-transfer? (get price resourceData) contract-caller (var-get paymentAddress))) ) ;; return new count (ok newCount) @@ -397,11 +306,106 @@ (pay-invoice (unwrap! (get-resource-index name) ERR_RESOURCE_NOT_FOUND) memo) ) + +;; read only functions +;; + +;; returns total registered users +(define-read-only (get-total-users) + (var-get userCount) +) + +;; returns user index for address if known +(define-read-only (get-user-index (user principal)) + (map-get? UserIndexes user) +) + +;; returns user data by user index if known +(define-read-only (get-user-data (index uint)) + (map-get? UserData index) +) + +;; returns user data by address if known +(define-read-only (get-user-data-by-address (user principal)) + (get-user-data (unwrap! (get-user-index user) none)) +) + +;; returns total registered resources +(define-read-only (get-total-resources) + (var-get resourceCount) +) + +;; returns resource index for name if known +(define-read-only (get-resource-index (name (string-utf8 50))) + (map-get? ResourceIndexes name) +) + +;; returns resource data by resource index if known +(define-read-only (get-resource (index uint)) + (map-get? ResourceData index) +) + +;; returns resource data by resource name if known +(define-read-only (get-resource-by-name (name (string-utf8 50))) + (get-resource (unwrap! (get-resource-index name) none)) +) + +;; returns total registered invoices +(define-read-only (get-total-invoices) + (var-get invoiceCount) +) + +;; returns invoice data by invoice index if known +(define-read-only (get-invoice (index uint)) + (map-get? InvoiceData index) +) + +;; returns invoice index by user index and resource index if known +(define-read-only (get-recent-payment (resourceIndex uint) (userIndex uint)) + (map-get? RecentPayments { + userIndex: userIndex, + resourceIndex: resourceIndex, + }) +) + +;; returns invoice data by user index and resource index if known +(define-read-only (get-recent-payment-data (resourceIndex uint) (userIndex uint)) + (get-invoice (unwrap! (get-recent-payment resourceIndex userIndex) none)) +) + +;; returns invoice data by user address and resource name if known +(define-read-only (get-recent-payment-data-by-address (name (string-utf8 50)) (user principal)) + (get-recent-payment-data (unwrap! (get-resource-index name) none) (unwrap! (get-user-index user) none)) +) + +;; returns payment address +(define-read-only (get-payment-address) + (some (var-get paymentAddress)) +) + +;; returns total revenue +(define-read-only (get-total-revenue) + (var-get totalRevenue) +) + +;; returns aggregate contract data +(define-read-only (get-contract-data) + { + paymentAddress: (get-payment-address), + totalInvoices: (get-total-invoices), + totalResources: (get-total-resources), + totalRevenue: (get-total-revenue), + totalUsers: (get-total-users) + } +) + ;; private functions ;; -(define-private (is-deployer) - (ok (asserts! (is-eq contract-caller DEPLOYER) ERR_UNAUTHORIZED)) +(define-private (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .aibtcdev-dao) + (contract-call? .aibtcdev-base-dao is-extension contract-caller)) ERR_UNAUTHORIZED + )) ) (define-private (get-or-create-user (address principal)) diff --git a/contracts/dao/extensions/aibtc-ext006-treasury.clar b/contracts/dao/extensions/aibtc-ext006-treasury.clar new file mode 100644 index 0000000..e7331c2 --- /dev/null +++ b/contracts/dao/extensions/aibtc-ext006-treasury.clar @@ -0,0 +1,231 @@ +;; title: aibtcdev-treasury +;; version: 1.0.0 +;; summary: An extension that manages STX, SIP-009 NFTs, and SIP-010 FTs. + +;; traits +;; +(impl-trait .aibtcdev-dao-traits-v1.extension) +(impl-trait .aibtcdev-dao-traits-v1.treasury) + +(use-trait ft-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) +(use-trait nft-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait) + +;; constants +;; + +(define-constant ERR_UNAUTHORIZED (err u6000)) +(define-constant ERR_UNKNOWN_ASSSET (err u6001)) +(define-constant TREASURY (as-contract tx-sender)) + +;; data maps +;; + +(define-map AllowedAssets principal bool) + +;; public functions +;; + +(define-public (callback (sender principal) (memo (buff 34))) + (ok true) +) + +;; add or update an asset to the allowed list +(define-public (allow-asset (token principal) (enabled bool)) + (begin + (try! (is-dao-or-extension)) + (print { + notification: "allow-asset", + payload: { + enabled: enabled, + token: token + } + }) + (ok (map-set AllowedAssets token enabled)) + ) +) + +;; add or update a list of assets to the allowed list +(define-public (allow-assets (allowList (list 100 {token: principal, enabled: bool}))) + (begin + (try! (is-dao-or-extension)) + (ok (map allow-assets-iter allowList)) + ) +) + +;; deposit STX to the treasury +(define-public (deposit-stx (amount uint)) + (begin + (print { + notification: "deposit-stx", + payload: { + amount: amount, + caller: contract-caller, + recipient: TREASURY, + sender: tx-sender + } + }) + (stx-transfer? amount tx-sender TREASURY) + ) +) + +;; deposit FT to the treasury +(define-public (deposit-ft (ft ) (amount uint)) + (begin + (asserts! (is-allowed-asset (contract-of ft)) ERR_UNKNOWN_ASSSET) + (print { + notification: "deposit-ft", + payload: { + amount: amount, + assetContract: (contract-of ft), + caller: contract-caller, + recipient: TREASURY, + sender: tx-sender + } + }) + (contract-call? ft transfer amount tx-sender TREASURY none) + ) +) + +;; deposit NFT to the treasury +(define-public (deposit-nft (nft ) (id uint)) + (begin + (asserts! (is-allowed-asset (contract-of nft)) ERR_UNKNOWN_ASSSET) + (print { + notification: "deposit-nft", + payload: { + assetContract: (contract-of nft), + caller: contract-caller, + recipient: TREASURY, + sender: tx-sender, + tokenId: id + } + }) + (contract-call? nft transfer id tx-sender TREASURY) + ) +) + +;; withdraw STX from the treasury +(define-public (withdraw-stx (amount uint) (recipient principal)) + (begin + (try! (is-dao-or-extension)) + (print { + notification: "withdraw-stx", + payload: { + amount: amount, + caller: contract-caller, + recipient: recipient, + sender: tx-sender + } + }) + (as-contract (stx-transfer? amount TREASURY recipient)) + ) +) + +;; withdraw FT from the treasury +(define-public (withdraw-ft (ft ) (amount uint) (recipient principal)) + (begin + (try! (is-dao-or-extension)) + (asserts! (is-allowed-asset (contract-of ft)) ERR_UNKNOWN_ASSSET) + (print { + notification: "withdraw-ft", + payload: { + assetContract: (contract-of ft), + caller: contract-caller, + recipient: recipient, + sender: tx-sender + } + }) + (as-contract (contract-call? ft transfer amount TREASURY recipient none)) + ) +) + +;; withdraw NFT from the treasury +(define-public (withdraw-nft (nft ) (id uint) (recipient principal)) + (begin + (try! (is-dao-or-extension)) + (asserts! (is-allowed-asset (contract-of nft)) ERR_UNKNOWN_ASSSET) + (print { + notification: "withdraw-nft", + payload: { + assetContract: (contract-of nft), + caller: contract-caller, + recipient: recipient, + sender: tx-sender, + tokenId: id + } + }) + (as-contract (contract-call? nft transfer id TREASURY recipient)) + ) +) + +;; delegate STX for stacking +(define-public (delegate-stx (maxAmount uint) (to principal)) + (begin + (try! (is-dao-or-extension)) + (print { + notification: "delegate-stx", + payload: { + amount: maxAmount, + caller: contract-caller, + delegate: to, + sender: tx-sender + } + }) + (match (as-contract (contract-call? 'SP000000000000000000002Q6VF78.pox-4 delegate-stx maxAmount to none none)) + success (ok success) + err (err (to-uint err)) + ) + ) +) + +;; revoke STX delegation, STX unlocks after cycle ends +(define-public (revoke-delegate-stx) + (begin + (try! (is-dao-or-extension)) + (print { + notification: "revoke-delegate-stx", + payload: { + caller: contract-caller, + sender: tx-sender + } + }) + (match (as-contract (contract-call? 'SP000000000000000000002Q6VF78.pox-4 revoke-delegate-stx)) + success (begin (print success) (ok true)) + err (err (to-uint err)) + ) + ) +) + +;; read only functions +;; + +(define-read-only (is-allowed-asset (assetContract principal)) + (default-to false (get-allowed-asset assetContract)) +) + +(define-read-only (get-allowed-asset (assetContract principal)) + (map-get? AllowedAssets assetContract) +) + +;; private functions +;; + +(define-private (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .aibtcdev-dao) + (contract-call? .aibtcdev-base-dao is-extension contract-caller)) ERR_UNAUTHORIZED + )) +) + +(define-private (allow-assets-iter (item {token: principal, enabled: bool})) + (begin + (print { + notification: "allow-asset", + payload: { + enabled: (get enabled item), + token: (get token item) + } + }) + (map-set AllowedAssets (get token item) (get enabled item)) + ) +) + diff --git a/contracts/dao/proposals/aibtc-prop001-bootstrap.clar b/contracts/dao/proposals/aibtc-prop001-bootstrap.clar new file mode 100644 index 0000000..853f927 --- /dev/null +++ b/contracts/dao/proposals/aibtc-prop001-bootstrap.clar @@ -0,0 +1,26 @@ +(impl-trait .aibtcdev-dao-traits-v1.proposal) + +(define-constant DAO_MANIFEST "This is where the DAO can put it's mission, purpose, and goals.") + +(define-public (execute (sender principal)) + (begin + ;; set initial extensions + (try! (contract-call? .aibtcdev-base-dao set-extensions + (list + {extension: .aibtc-ext001-actions, enabled: true} + {extension: .aibtc-ext002-bank-account, enabled: true} + {extension: .aibtc-ext003-direct-execute, enabled: true} + {extension: .aibtc-ext004-messaging, enabled: true} + {extension: .aibtc-ext005-payments, enabled: true} + {extension: .aibtc-ext006-treasury, enabled: true} + ) + )) + ;; print manifest + (print DAO_MANIFEST) + (ok true) + ) +) + +(define-read-only (get-dao-manifest) + DAO_MANIFEST +) diff --git a/contracts/dao/traits/aibtcdev-dao-traits-v1.clar b/contracts/dao/traits/aibtcdev-dao-traits-v1.clar new file mode 100644 index 0000000..708f9d8 --- /dev/null +++ b/contracts/dao/traits/aibtcdev-dao-traits-v1.clar @@ -0,0 +1,166 @@ +;; title: aibtcdev-dao-traits-v1 +;; version: 1.0.0 +;; summary: A collection of traits for the aibtcdev DAO + +;; IMPORTS + +(use-trait ft-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) +(use-trait nft-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait) + +;; CORE DAO TRAITS + +(define-trait proposal ( + (execute (principal) (response bool uint)) +)) + +(define-trait extension ( + (callback (principal (buff 34)) (response bool uint)) +)) + +;; EXTENSION TRAITS + +(define-trait bank-account ( + ;; set account holder + ;; @param principal the new account holder + ;; @returns (response bool uint) + (set-account-holder (principal) (response bool uint)) + ;; set withdrawal period + ;; @param period the new withdrawal period in blocks + ;; @returns (response bool uint) + (set-withdrawal-period (uint) (response bool uint)) + ;; set withdrawal amount + ;; @param amount the new withdrawal amount in microSTX + ;; @returns (response bool uint) + (set-withdrawal-amount (uint) (response bool uint)) + ;; override last withdrawal block + ;; @param block the new last withdrawal block + ;; @returns (response bool uint) + (override-last-withdrawal-block (uint) (response bool uint)) + ;; deposit STX to the bank account + ;; @param amount amount of microSTX to deposit + ;; @returns (response bool uint) + (deposit-stx (uint) (response bool uint)) + ;; withdraw STX from the bank account + ;; @returns (response bool uint) + (withdraw-stx () (response bool uint)) +)) + +(define-trait messaging ( + ;; send a message on-chain (opt from DAO) + ;; @param msg the message to send (up to 1MB) + ;; @param isFromDao whether the message is from the DAO + ;; @returns (response bool uint) + (send ((string-ascii 1048576) bool) (response bool uint)) +)) + +(define-trait resources ( + ;; set payment address for resource invoices + ;; @param principal the new payment address + ;; @returns (response bool uint) + (set-payment-address (principal) (response bool uint)) + ;; adds a new resource that users can pay for + ;; @param name the name of the resource (unique!) + ;; @param price the price of the resource in microSTX + ;; @param description a description of the resource + ;; @returns (response uint uint) + (add-resource ((string-utf8 50) (string-utf8 255) uint (optional (string-utf8 255))) (response uint uint)) + ;; toggles a resource on or off for payment + ;; @param resource the ID of the resource + ;; @returns (response bool uint) + (toggle-resource (uint) (response bool uint)) + ;; toggles a resource on or off for payment by name + ;; @param name the name of the resource + ;; @returns (response bool uint) + (toggle-resource-by-name ((string-utf8 50)) (response bool uint)) +)) + +(define-trait invoices ( + ;; pay an invoice by ID + ;; @param invoice the ID of the invoice + ;; @returns (response uint uint) + (pay-invoice (uint (optional (buff 34))) (response uint uint)) + ;; pay an invoice by resource name + ;; @param name the name of the resource + ;; @returns (response uint uint) + (pay-invoice-by-resource-name ((string-utf8 50) (optional (buff 34))) (response uint uint)) +)) + +(define-trait treasury ( + ;; allow an asset for deposit/withdrawal + ;; @param token the asset contract principal + ;; @param enabled whether the asset is allowed + ;; @returns (response bool uint) + (allow-asset (principal bool) (response bool uint)) + ;; allow multiple assets for deposit/withdrawal + ;; @param allowList a list of asset contracts and enabled status + ;; @returns (response bool uint) + ;; TODO: removed due to conflict with contract definition (both are the same?) + ;; (allow-assets ((list 100 (tuple (token principal) (enabled bool)))) (response bool uint)) + ;; deposit STX to the treasury + ;; @param amount amount of microSTX to deposit + ;; @returns (response bool uint) + (deposit-stx (uint) (response bool uint)) + ;; deposit FT to the treasury + ;; @param ft the fungible token contract principal + ;; @param amount amount of tokens to deposit + ;; @returns (response bool uint) + (deposit-ft ( uint) (response bool uint)) + ;; deposit NFT to the treasury + ;; @param nft the non-fungible token contract principal + ;; @param id the ID of the token to deposit + ;; @returns (response bool uint) + (deposit-nft ( uint) (response bool uint)) + ;; withdraw STX from the treasury + ;; @param amount amount of microSTX to withdraw + ;; @param recipient the recipient of the STX + ;; @returns (response bool uint) + (withdraw-stx (uint principal) (response bool uint)) + ;; withdraw FT from the treasury + ;; @param ft the fungible token contract principal + ;; @param amount amount of tokens to withdraw + ;; @param recipient the recipient of the tokens + ;; @returns (response bool uint) + (withdraw-ft ( uint principal) (response bool uint)) + ;; withdraw NFT from the treasury + ;; @param nft the non-fungible token contract principal + ;; @param id the ID of the token to withdraw + ;; @param recipient the recipient of the token + ;; @returns (response bool uint) + (withdraw-nft ( uint principal) (response bool uint)) + ;; delegate STX for stacking in PoX + ;; @param amount max amount of microSTX that can be delegated + ;; @param to the address to delegate to + ;; @returns (response bool uint) + (delegate-stx (uint principal) (response bool uint)) + ;; revoke delegation of STX from stacking in PoX + ;; @returns (response bool uint) + (revoke-delegate-stx () (response bool uint)) +)) + +(define-trait direct-execute ( + ;; set the protocol treasury contract + ;; @param treasury the treasury contract principal + ;; @returns (response bool uint) + (set-protocol-treasury () (response bool uint)) + ;; set the voting token contract + ;; @param token the token contract principal + ;; @returns (response bool uint) + (set-voting-token () (response bool uint)) + ;; create a new proposal + ;; @param proposal the proposal contract + ;; @param token the voting token contract + ;; @returns (response bool uint) + (create-proposal ( ) (response bool uint)) + ;; vote on an existing proposal + ;; @param proposal the proposal contract + ;; @param token the voting token contract + ;; @param vote true for yes, false for no + ;; @returns (response bool uint) + (vote-on-proposal ( bool) (response bool uint)) + ;; conclude a proposal after voting period + ;; @param proposal the proposal contract + ;; @param treasury the treasury contract + ;; @param token the voting token contract + ;; @returns (response bool uint) + (conclude-proposal ( ) (response bool uint)) +)) diff --git a/contracts/dao/traits/aibtcdev-dao-v1.clar b/contracts/dao/traits/aibtcdev-dao-v1.clar new file mode 100644 index 0000000..ab9eb78 --- /dev/null +++ b/contracts/dao/traits/aibtcdev-dao-v1.clar @@ -0,0 +1,11 @@ +(use-trait proposal-trait .aibtcdev-dao-traits-v1.proposal) +(use-trait extension-trait .aibtcdev-dao-traits-v1.extension) + +(define-trait aibtcdev-base-dao ( + ;; Execute a governance proposal + (execute ( principal) (response bool uint)) + ;; Enable or disable an extension contract + (set-extension (principal bool) (response bool uint)) + ;; Request extension callback + (request-extension-callback ( (buff 34)) (response bool uint)) +)) diff --git a/contracts/test/aibtc-treasury.clar b/contracts/test/aibtc-treasury.clar new file mode 100644 index 0000000..ead65a2 --- /dev/null +++ b/contracts/test/aibtc-treasury.clar @@ -0,0 +1,49 @@ +;; test treasury contract implementing treasury trait +(impl-trait .aibtcdev-dao-traits-v1.treasury) + +(use-trait ft-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) +(use-trait nft-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait) + +(define-public (callback (sender principal) (memo (buff 34))) + (ok true) +) + +(define-public (allow-asset (token principal) (enabled bool)) + (ok true) +) + +(define-public (allow-assets (allowList (list 100 {token: principal, enabled: bool}))) + (ok true) +) + +(define-public (deposit-stx (amount uint)) + (ok true) +) + +(define-public (deposit-ft (ft ) (amount uint)) + (ok true) +) + +(define-public (deposit-nft (nft ) (id uint)) + (ok true) +) + +(define-public (withdraw-stx (amount uint) (recipient principal)) + (ok true) +) + +(define-public (withdraw-ft (ft ) (amount uint) (recipient principal)) + (ok true) +) + +(define-public (withdraw-nft (nft ) (id uint) (recipient principal)) + (ok true) +) + +(define-public (delegate-stx (maxAmount uint) (to principal)) + (ok true) +) + +(define-public (revoke-delegate-stx) + (ok true) +) diff --git a/contracts/test-proxy.clar b/contracts/test/proxy.clar similarity index 51% rename from contracts/test-proxy.clar rename to contracts/test/proxy.clar index 36dfd7c..293dfa0 100644 --- a/contracts/test-proxy.clar +++ b/contracts/test/proxy.clar @@ -1,38 +1,13 @@ ;; title: test-proxy -;; version: -;; summary: -;; description: - -;; traits -;; - -;; token definitions -;; ;; constants ;; (define-constant CONTRACT (as-contract tx-sender)) (define-constant OWNER tx-sender) -;; data vars -;; - -;; data maps -;; - ;; public functions ;; -(define-public (get-standard-caller) - (begin - (print { - caller: contract-caller, - sender: tx-sender, - }) - (ok (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.aibtcdev-bank-account get-standard-caller)) - ) -) - (define-public (mint-aibtcdev-1 (to principal)) (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.aibtcdev-airdrop-1 mint to) ) @@ -40,10 +15,3 @@ (define-public (mint-aibtcdev-2 (to principal)) (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.aibtcdev-airdrop-2 mint to) ) - -;; read only functions -;; - -;; private functions -;; - diff --git a/contracts/test/sip010-token.clar b/contracts/test/sip010-token.clar new file mode 100644 index 0000000..2881c86 --- /dev/null +++ b/contracts/test/sip010-token.clar @@ -0,0 +1,39 @@ +;; test token contract implementing ft trait +(impl-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) + +(define-fungible-token test-token) + +(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34)))) + (ok true) +) + +(define-read-only (get-name) + (ok "Test Token") +) + +(define-read-only (get-symbol) + (ok "TEST") +) + +(define-read-only (get-decimals) + (ok u6) +) + +(define-read-only (get-balance (who principal)) + (ok u0) +) + +(define-read-only (get-total-supply) + (ok u0) +) + +(define-read-only (get-token-uri) + (ok none) +) + +;; Test helper functions +(define-public (mint (amount uint) (recipient principal)) + (begin + (ft-mint? test-token amount recipient) + ) +) diff --git a/deployments/default.simnet-plan.yaml b/deployments/default.simnet-plan.yaml index cdb06a0..c99ee84 100644 --- a/deployments/default.simnet-plan.yaml +++ b/deployments/default.simnet-plan.yaml @@ -57,6 +57,11 @@ plan: emulated-sender: SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9 path: "./.cache/requirements/SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.clar" clarity-version: 1 + - emulated-contract-publish: + contract-name: sip-010-trait-ft-standard + emulated-sender: SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE + path: "./.cache/requirements/SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.clar" + clarity-version: 1 epoch: "2.1" - id: 2 transactions: [] @@ -122,88 +127,168 @@ plan: transactions: [] epoch: "2.1" - id: 23 - transactions: - - emulated-contract-publish: - contract-name: aibtcdev-aibtc - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/aibtcdev-aibtc.clar - clarity-version: 2 - - emulated-contract-publish: - contract-name: aibtcdev-traits-v1 - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/aibtcdev-traits-v1.clar - clarity-version: 2 - - emulated-contract-publish: - contract-name: aibtcdev-resources-v1 - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/aibtcdev-resources-v1.clar - clarity-version: 2 - epoch: "2.4" + transactions: [] + epoch: "2.1" - id: 24 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 25 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 26 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 27 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 28 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 29 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 30 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 31 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 32 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 33 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 34 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 35 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 36 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 37 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 38 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 39 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 40 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 41 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 42 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 43 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 44 transactions: [] - epoch: "2.4" + epoch: "2.1" - id: 45 + transactions: [] + epoch: "2.1" + - id: 46 + transactions: [] + epoch: "2.1" + - id: 47 + transactions: [] + epoch: "2.1" + - id: 48 + transactions: [] + epoch: "2.1" + - id: 49 + transactions: [] + epoch: "2.1" + - id: 50 + transactions: [] + epoch: "2.1" + - id: 51 + transactions: [] + epoch: "2.1" + - id: 52 + transactions: [] + epoch: "2.1" + - id: 53 + transactions: [] + epoch: "2.1" + - id: 54 + transactions: [] + epoch: "2.1" + - id: 55 + transactions: [] + epoch: "2.1" + - id: 56 + transactions: [] + epoch: "2.1" + - id: 57 + transactions: [] + epoch: "2.1" + - id: 58 + transactions: [] + epoch: "2.1" + - id: 59 + transactions: [] + epoch: "2.1" + - id: 60 transactions: + - emulated-contract-publish: + contract-name: aibtcdev-dao-traits-v1 + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/dao/traits/aibtcdev-dao-traits-v1.clar + clarity-version: 2 + - emulated-contract-publish: + contract-name: aibtcdev-dao-v1 + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/dao/traits/aibtcdev-dao-v1.clar + clarity-version: 2 + - emulated-contract-publish: + contract-name: aibtcdev-base-dao + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/dao/aibtcdev-base-dao.clar + clarity-version: 2 + - emulated-contract-publish: + contract-name: aibtc-ext001-actions + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/dao/extensions/aibtc-ext001-actions.clar + clarity-version: 2 + - emulated-contract-publish: + contract-name: aibtc-ext002-bank-account + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/dao/extensions/aibtc-ext002-bank-account.clar + clarity-version: 2 + - emulated-contract-publish: + contract-name: aibtc-ext003-direct-execute + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/dao/extensions/aibtc-ext003-direct-execute.clar + clarity-version: 2 + - emulated-contract-publish: + contract-name: aibtc-ext004-messaging + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/dao/extensions/aibtc-ext004-messaging.clar + clarity-version: 2 + - emulated-contract-publish: + contract-name: aibtc-ext005-payments + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/dao/extensions/aibtc-ext005-payments.clar + clarity-version: 2 + - emulated-contract-publish: + contract-name: aibtc-ext006-treasury + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/dao/extensions/aibtc-ext006-treasury.clar + clarity-version: 2 + - emulated-contract-publish: + contract-name: aibtc-prop001-bootstrap + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/dao/proposals/aibtc-prop001-bootstrap.clar + clarity-version: 2 - emulated-contract-publish: contract-name: aibtcdev-airdrop-1 emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM @@ -215,91 +300,196 @@ plan: path: contracts/aibtcdev-airdrop-2.clar clarity-version: 2 - emulated-contract-publish: - contract-name: aibtcdev-bank-account + contract-name: proxy emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/aibtcdev-bank-account.clar + path: contracts/test/proxy.clar clarity-version: 2 - emulated-contract-publish: - contract-name: aibtcdev-messaging + contract-name: test-proxy emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/aibtcdev-messaging.clar + path: contracts/test/proxy.clar clarity-version: 2 - emulated-contract-publish: - contract-name: proxy + contract-name: test-token emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/test-proxy.clar + path: contracts/test/sip010-token.clar clarity-version: 2 - emulated-contract-publish: - contract-name: test-proxy + contract-name: test-treasury emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/test-proxy.clar + path: contracts/test/aibtc-treasury.clar clarity-version: 2 - emulated-contract-publish: contract-name: external-proxy emulated-sender: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 - path: contracts/test-proxy.clar + path: contracts/test/proxy.clar clarity-version: 2 epoch: "2.5" - - id: 46 + - id: 61 transactions: [] epoch: "2.5" - - id: 47 + - id: 62 transactions: [] epoch: "2.5" - - id: 48 + - id: 63 transactions: [] epoch: "2.5" - - id: 49 + - id: 64 transactions: [] epoch: "2.5" - - id: 50 + - id: 65 transactions: [] epoch: "2.5" - - id: 51 + - id: 66 transactions: [] epoch: "2.5" - - id: 52 + - id: 67 transactions: [] epoch: "2.5" - - id: 53 + - id: 68 transactions: [] epoch: "2.5" - - id: 54 + - id: 69 transactions: [] epoch: "2.5" - - id: 55 + - id: 70 transactions: [] epoch: "2.5" - - id: 56 + - id: 71 transactions: [] epoch: "2.5" - - id: 57 + - id: 72 transactions: [] epoch: "2.5" - - id: 58 + - id: 73 transactions: [] epoch: "2.5" - - id: 59 + - id: 74 transactions: [] epoch: "2.5" - - id: 60 + - id: 75 transactions: [] epoch: "2.5" - - id: 61 + - id: 76 transactions: [] epoch: "2.5" - - id: 62 + - id: 77 transactions: [] epoch: "2.5" - - id: 63 + - id: 78 transactions: [] epoch: "2.5" - - id: 64 + - id: 79 transactions: [] epoch: "2.5" - - id: 65 + - id: 80 transactions: [] epoch: "2.5" - - id: 66 + - id: 81 + transactions: [] + epoch: "2.5" + - id: 82 + transactions: [] + epoch: "2.5" + - id: 83 + transactions: [] + epoch: "2.5" + - id: 84 + transactions: [] + epoch: "2.5" + - id: 85 + transactions: [] + epoch: "2.5" + - id: 86 + transactions: [] + epoch: "2.5" + - id: 87 + transactions: [] + epoch: "2.5" + - id: 88 + transactions: [] + epoch: "2.5" + - id: 89 + transactions: [] + epoch: "2.5" + - id: 90 + transactions: [] + epoch: "2.5" + - id: 91 + transactions: [] + epoch: "2.5" + - id: 92 + transactions: [] + epoch: "2.5" + - id: 93 + transactions: [] + epoch: "2.5" + - id: 94 + transactions: [] + epoch: "2.5" + - id: 95 + transactions: [] + epoch: "2.5" + - id: 96 + transactions: [] + epoch: "2.5" + - id: 97 + transactions: [] + epoch: "2.5" + - id: 98 + transactions: [] + epoch: "2.5" + - id: 99 + transactions: [] + epoch: "2.5" + - id: 100 + transactions: [] + epoch: "2.5" + - id: 101 + transactions: [] + epoch: "2.5" + - id: 102 + transactions: [] + epoch: "2.5" + - id: 103 + transactions: [] + epoch: "2.5" + - id: 104 + transactions: [] + epoch: "2.5" + - id: 105 + transactions: [] + epoch: "2.5" + - id: 106 + transactions: [] + epoch: "2.5" + - id: 107 + transactions: [] + epoch: "2.5" + - id: 108 + transactions: [] + epoch: "2.5" + - id: 109 + transactions: [] + epoch: "2.5" + - id: 110 + transactions: [] + epoch: "2.5" + - id: 111 + transactions: [] + epoch: "2.5" + - id: 112 + transactions: [] + epoch: "2.5" + - id: 113 + transactions: [] + epoch: "2.5" + - id: 114 + transactions: [] + epoch: "2.5" + - id: 115 + transactions: [] + epoch: "2.5" + - id: 116 transactions: [] epoch: "2.5" diff --git a/docs/dao/README.md b/docs/dao/README.md new file mode 100644 index 0000000..76f6e20 --- /dev/null +++ b/docs/dao/README.md @@ -0,0 +1,45 @@ +# aibtcdev DAO Documentation + +This directory contains documentation for the aibtcdev DAO smart contracts. + +## Core Components + +- [Base DAO](base-dao.md) - The main DAO contract that manages extensions and proposal execution +- [Extensions](extensions/README.md) - Modular components that add functionality to the DAO +- [Proposals](proposals/README.md) - Contracts that can be executed by the DAO + +## Architecture Overview + +The aibtcdev DAO follows an "executor DAO" pattern where: + +1. The base DAO contract manages a set of extensions and can execute proposals +2. Extensions provide specific functionality (payments, messaging, etc) +3. Proposals are contracts that can be executed by the DAO to make changes + +This modular architecture allows: + +- Adding new functionality through extensions +- Upgrading components independently +- Fine-grained access control +- Clear separation of concerns + +## Error Codes + +Each contract uses a distinct error code range to make debugging easier: + +- Base DAO: 900-999 +- Actions Extension: 1000-1999 +- Bank Account Extension: 2000-2999 +- Direct Execute Extension: 3000-3999 +- Messaging Extension: 4000-4999 +- Payments Extension: 5000-5999 +- Treasury Extension: 6000-6999 + +## Getting Started + +See the individual component documentation for details on: + +- Contract interfaces and functions +- Extension capabilities +- Creating and executing proposals +- Managing DAO settings diff --git a/docs/dao/base-dao.md b/docs/dao/base-dao.md new file mode 100644 index 0000000..7e7d756 --- /dev/null +++ b/docs/dao/base-dao.md @@ -0,0 +1,96 @@ +# Base DAO Contract + +The base DAO contract (`aibtcdev-base-dao.clar`) is the core contract that manages extensions and proposal execution. + +## Key Features + +- Manages enabled/disabled status of extensions +- Executes proposals that can modify DAO state +- Controls access to extension functionality +- Maintains record of executed proposals + +## Constants + +- `ERR_UNAUTHORIZED (900)` - Caller not authorized +- `ERR_ALREADY_EXECUTED (901)` - Proposal already executed +- `ERR_INVALID_EXTENSION (902)` - Extension validation failed +- `ERR_NO_EMPTY_LISTS (903)` - Empty list provided + +## Storage + +### Data Variables + +- `executive` - Principal that can construct the DAO, set to contract itself after construction + +### Maps + +- `ExecutedProposals` - Tracks block height when proposals were executed +- `Extensions` - Tracks enabled/disabled status of extensions + +## Functions + +### Public Functions + +#### construct +```clarity +(define-public (construct (proposal ))) +``` +Initial construction of the DAO. Can only be called once by the executive. + +#### execute +```clarity +(define-public (execute (proposal ) (sender principal))) +``` +Executes a proposal contract. Can only be called by the DAO or enabled extensions. + +#### set-extension +```clarity +(define-public (set-extension (extension principal) (enabled bool))) +``` +Enables or disables an extension. Can only be called by the DAO or enabled extensions. + +#### set-extensions +```clarity +(define-public (set-extensions (extensionList (list 200 {extension: principal, enabled: bool})))) +``` +Enables or disables multiple extensions. Can only be called by the DAO or enabled extensions. + +#### request-extension-callback +```clarity +(define-public (request-extension-callback (extension ) (memo (buff 34)))) +``` +Requests a callback from an extension. Can only be called by enabled extensions. + +### Read-Only Functions + +#### is-extension +```clarity +(define-read-only (is-extension (extension principal))) +``` +Returns whether an extension is enabled. + +#### executed-at +```clarity +(define-read-only (executed-at (proposal ))) +``` +Returns the block height when a proposal was executed, if it was. + +## Usage Examples + +### Enabling an Extension + +```clarity +(contract-call? .aibtcdev-base-dao set-extension .aibtc-ext001-actions true) +``` + +### Executing a Proposal + +```clarity +(contract-call? .aibtcdev-base-dao execute .aibtc-prop001-bootstrap tx-sender) +``` + +### Checking Extension Status + +```clarity +(contract-call? .aibtcdev-base-dao is-extension .aibtc-ext001-actions) +``` diff --git a/docs/dao/extensions/README.md b/docs/dao/extensions/README.md new file mode 100644 index 0000000..a123f90 --- /dev/null +++ b/docs/dao/extensions/README.md @@ -0,0 +1,70 @@ +# DAO Extensions + +Extensions are modular components that add functionality to the DAO. Each extension implements specific traits and can only be called by the DAO or other enabled extensions. + +## Available Extensions + +- [Actions](aibtc-ext001-actions.md) - Voting on predefined actions +- [Bank Account](aibtc-ext002-bank-account.md) - Managed STX withdrawals +- [Direct Execute](aibtc-ext003-direct-execute.md) - Voting on arbitrary code execution +- [Messaging](aibtc-ext004-messaging.md) - On-chain messaging up to 1MB +- [Payments](aibtc-ext005-payments.md) - Payment processing for DAO services with invoicing +- [Treasury](aibtc-ext006-treasury.md) - Multi-asset treasury management (STX, NFTs, FTs) + +## Extension Architecture + +Each extension: + +1. Implements the base extension trait +2. Implements additional specialized traits +3. Uses a unique error code range +4. Can only be called by the DAO or other extensions +5. Can request callbacks from other extensions + +## Common Patterns + +### Authorization Check + +All extensions use this pattern to verify calls: + +```clarity +(define-private (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .aibtcdev-dao) + (contract-call? .aibtcdev-base-dao is-extension contract-caller)) ERR_UNAUTHORIZED + )) +) +``` + +### Callback Support + +Extensions implement a callback function: + +```clarity +(define-public (callback (sender principal) (memo (buff 34))) + (ok true) +) +``` + +### Event Logging + +Extensions log events with consistent format: + +```clarity +(print { + notification: "event-name", + payload: { + key1: value1, + key2: value2 + } +}) +``` + +## Adding New Extensions + +To add a new extension: + +1. Create contract implementing required traits +2. Use unique error code range +3. Add authorization checks +4. Implement callback support +5. Add to bootstrap proposal diff --git a/docs/dao/extensions/aibtc-ext002-bank-account.md b/docs/dao/extensions/aibtc-ext002-bank-account.md new file mode 100644 index 0000000..849202e --- /dev/null +++ b/docs/dao/extensions/aibtc-ext002-bank-account.md @@ -0,0 +1,83 @@ +# Bank Account Extension + +The bank account extension (`aibtc-ext002-bank-account.clar`) enables managed STX withdrawals with configurable periods and amounts. + +## Key Features + +- Configurable withdrawal period (default: 144 blocks, ~1 day) +- Configurable withdrawal amount (default: 10 STX) +- Single account holder +- Deposit support for any user +- Withdrawal tracking and limits + +## Error Codes + +- `ERR_INVALID (2000)` - Invalid parameter value +- `ERR_UNAUTHORIZED (2001)` - Caller not authorized +- `ERR_TOO_SOON (2002)` - Withdrawal period not elapsed +- `ERR_INVALID_AMOUNT (2003)` - Invalid amount specified + +## Functions + +### Configuration + +#### set-account-holder +```clarity +(set-account-holder (new principal)) +``` +Sets the account holder who can make withdrawals. DAO/extension only. + +#### set-withdrawal-period +```clarity +(set-withdrawal-period (period uint)) +``` +Sets the required blocks between withdrawals. DAO/extension only. + +#### set-withdrawal-amount +```clarity +(set-withdrawal-amount (amount uint)) +``` +Sets the STX amount per withdrawal. DAO/extension only. + +### Operations + +#### deposit-stx +```clarity +(deposit-stx (amount uint)) +``` +Allows any user to deposit STX to the contract. + +#### withdraw-stx +```clarity +(withdraw-stx) +``` +Allows account holder to withdraw configured amount if period elapsed. + +### Read-Only Functions + +- `get-account-balance` - Current STX balance +- `get-account-holder` - Current account holder +- `get-withdrawal-period` - Current withdrawal period +- `get-withdrawal-amount` - Current withdrawal amount +- `get-last-withdrawal-block` - Block height of last withdrawal +- `get-account-terms` - All account settings and state + +## Usage Examples + +### Depositing STX + +```clarity +(contract-call? .aibtc-ext002-bank-account deposit-stx u1000000) +``` + +### Withdrawing STX (as account holder) + +```clarity +(contract-call? .aibtc-ext002-bank-account withdraw-stx) +``` + +### Checking Account Terms + +```clarity +(contract-call? .aibtc-ext002-bank-account get-account-terms) +``` diff --git a/docs/dao/extensions/aibtc-ext003-direct-execute.md b/docs/dao/extensions/aibtc-ext003-direct-execute.md new file mode 100644 index 0000000..dfd86f7 --- /dev/null +++ b/docs/dao/extensions/aibtc-ext003-direct-execute.md @@ -0,0 +1,113 @@ +# Direct Execute Extension + +The direct execute extension (`aibtc-ext003-direct-execute.clar`) enables voting on proposals to execute arbitrary Clarity code in the context of the DAO. + +## Key Features + +- Token-weighted voting using SIP-010 tokens +- Configurable voting period (144 blocks, ~1 day) +- High quorum requirement (95% of liquid supply) +- Proposal execution tracking +- Vote recording per proposal/voter + +## Error Codes + +### Authorization +- `ERR_UNAUTHORIZED (3000)` - Caller not authorized +- `ERR_NOT_DAO_OR_EXTENSION (3001)` - Not called by DAO or extension + +### Initialization +- `ERR_NOT_INITIALIZED (3100)` - Required settings not configured +- `ERR_ALREADY_INITIALIZED (3101)` - Settings already configured + +### Treasury +- `ERR_TREASURY_MUST_BE_CONTRACT (3200)` - Treasury must be contract +- `ERR_TREASURY_CANNOT_BE_SELF (3201)` - Treasury cannot be self +- `ERR_TREASURY_ALREADY_SET (3202)` - Treasury already configured +- `ERR_TREASURY_MISMATCH (3203)` - Treasury does not match + +### Voting Token +- `ERR_TOKEN_MUST_BE_CONTRACT (3300)` - Token must be contract +- `ERR_TOKEN_NOT_INITIALIZED (3301)` - Token not configured +- `ERR_TOKEN_MISMATCH (3302)` - Token does not match +- `ERR_INSUFFICIENT_BALANCE (3303)` - Insufficient token balance + +### Proposals +- `ERR_PROPOSAL_NOT_FOUND (3400)` - Proposal not found +- `ERR_PROPOSAL_ALREADY_EXECUTED (3401)` - Already executed +- `ERR_PROPOSAL_STILL_ACTIVE (3402)` - Still in voting period +- `ERR_SAVING_PROPOSAL (3403)` - Error saving proposal +- `ERR_PROPOSAL_ALREADY_CONCLUDED (3404)` - Already concluded + +### Voting +- `ERR_VOTE_TOO_SOON (3500)` - Before start block +- `ERR_VOTE_TOO_LATE (3501)` - After end block +- `ERR_ALREADY_VOTED (3502)` - Already voted +- `ERR_ZERO_VOTING_POWER (3503)` - No voting power +- `ERR_QUORUM_NOT_REACHED (3504)` - Quorum not met + +## Functions + +### Configuration + +#### set-protocol-treasury +```clarity +(set-protocol-treasury (treasury )) +``` +Sets the treasury contract. DAO/extension only. + +#### set-voting-token +```clarity +(set-voting-token (token )) +``` +Sets the SIP-010 token used for voting. DAO/extension only. + +### Proposals + +#### create-proposal +```clarity +(create-proposal (proposal ) (token )) +``` +Creates a new proposal for voting. + +#### vote-on-proposal +```clarity +(vote-on-proposal (proposal ) (token ) (vote bool)) +``` +Casts a vote on a proposal. + +#### conclude-proposal +```clarity +(conclude-proposal (proposal ) (treasury ) (token )) +``` +Concludes voting and executes if passed. + +### Read-Only Functions + +- `get-protocol-treasury` - Current treasury contract +- `get-voting-token` - Current voting token +- `get-proposal` - Proposal details +- `get-total-votes` - Vote count for proposal/voter +- `is-initialized` - Configuration status +- `get-voting-period` - Current voting period +- `get-voting-quorum` - Required quorum percentage + +## Usage Examples + +### Creating a Proposal + +```clarity +(contract-call? .aibtc-ext003-direct-execute create-proposal .my-proposal .voting-token) +``` + +### Voting on a Proposal + +```clarity +(contract-call? .aibtc-ext003-direct-execute vote-on-proposal .my-proposal .voting-token true) +``` + +### Concluding a Proposal + +```clarity +(contract-call? .aibtc-ext003-direct-execute conclude-proposal .my-proposal .treasury .voting-token) +``` diff --git a/docs/dao/extensions/aibtc-ext004-messaging.md b/docs/dao/extensions/aibtc-ext004-messaging.md new file mode 100644 index 0000000..8988e04 --- /dev/null +++ b/docs/dao/extensions/aibtc-ext004-messaging.md @@ -0,0 +1,79 @@ +# Messaging Extension + +The messaging extension (`aibtc-ext004-messaging.clar`) enables on-chain messaging capabilities for the DAO and authorized extensions. + +## Key Features + +- Send messages up to 1MB in length +- Messages are recorded on-chain via print events +- Messages can be sent by DAO or extensions +- Each message includes sender metadata +- Message envelope contains block height and caller info + +## Error Codes + +- `ERR_UNAUTHORIZED (4000)` - Caller not authorized +- `INPUT_ERROR (400)` - Invalid message format/length + +## Functions + +### Public Functions + +#### send +```clarity +(send (msg (string-ascii 1048576)) (isFromDao bool)) +``` +Sends a message on-chain. If isFromDao is true, requires DAO/extension authorization. + +#### callback +```clarity +(callback (sender principal) (memo (buff 34))) +``` +Standard extension callback support. + +### Private Functions + +#### is-dao-or-extension +```clarity +(is-dao-or-extension) +``` +Verifies caller is DAO or enabled extension. + +## Message Format + +Messages are logged in two parts: + +1. The message content itself +2. An envelope with metadata: +```json +{ + "notification": "send", + "payload": { + "caller": "", + "height": "", + "isFromDao": "", + "sender": "" + } +} +``` + +## Usage Examples + +### Sending a Message (as DAO/extension) + +```clarity +(contract-call? .aibtc-ext004-messaging send "Hello World" true) +``` + +### Sending a Message (as regular user) + +```clarity +(contract-call? .aibtc-ext004-messaging send "Hello World" false) +``` + +### Reading Messages + +Messages can be read by: +1. Watching for print events from the contract +2. Querying historical print events +3. Using event indexing services diff --git a/docs/dao/extensions/aibtc-ext005-payments.md b/docs/dao/extensions/aibtc-ext005-payments.md new file mode 100644 index 0000000..05c5f3d --- /dev/null +++ b/docs/dao/extensions/aibtc-ext005-payments.md @@ -0,0 +1,134 @@ +# Payments Extension + +The payments extension (`aibtc-ext005-payments.clar`) provides payment processing capabilities for aibtcdev services. + +## Key Features + +- Resource management with pricing +- Invoice generation and tracking +- User payment history +- Revenue tracking +- Configurable payment address + +## Error Codes + +### Authorization +- `ERR_UNAUTHORIZED (5000)` - Caller not authorized +- `ERR_INVALID_PARAMS (5001)` - Invalid parameters provided + +### Resources +- `ERR_NAME_ALREADY_USED (5002)` - Resource name already exists +- `ERR_SAVING_RESOURCE_DATA (5003)` - Error saving resource data +- `ERR_DELETING_RESOURCE_DATA (5004)` - Error deleting resource +- `ERR_RESOURCE_NOT_FOUND (5005)` - Resource does not exist +- `ERR_RESOURCE_DISABLED (5006)` - Resource is disabled + +### Users +- `ERR_USER_ALREADY_EXISTS (5007)` - User already registered +- `ERR_SAVING_USER_DATA (5008)` - Error saving user data +- `ERR_USER_NOT_FOUND (5009)` - User does not exist + +### Invoices +- `ERR_INVOICE_ALREADY_PAID (5010)` - Invoice already paid +- `ERR_SAVING_INVOICE_DATA (5011)` - Error saving invoice +- `ERR_INVOICE_NOT_FOUND (5012)` - Invoice does not exist +- `ERR_RECENT_PAYMENT_NOT_FOUND (5013)` - No recent payment found + +## Functions + +### Resource Management + +#### add-resource +```clarity +(add-resource (name (string-utf8 50)) (description (string-utf8 255)) (price uint) (url (optional (string-utf8 255)))) +``` +Adds a new resource with pricing. DAO/extension only. + +#### toggle-resource +```clarity +(toggle-resource (resourceIndex uint)) +``` +Enables/disables a resource. DAO/extension only. + +#### toggle-resource-by-name +```clarity +(toggle-resource-by-name (name (string-utf8 50))) +``` +Enables/disables a resource by name. DAO/extension only. + +### Payment Processing + +#### pay-invoice +```clarity +(pay-invoice (resourceIndex uint) (memo (optional (buff 34)))) +``` +Processes payment for a resource. + +#### pay-invoice-by-resource-name +```clarity +(pay-invoice-by-resource-name (name (string-utf8 50)) (memo (optional (buff 34)))) +``` +Processes payment using resource name. + +#### set-payment-address +```clarity +(set-payment-address (newAddress principal)) +``` +Updates payment destination. DAO/extension only. + +### Read-Only Functions + +#### Resource Info +- `get-total-resources` - Number of resources +- `get-resource-index` - Get resource ID by name +- `get-resource` - Get resource details by ID +- `get-resource-by-name` - Get resource details by name + +#### User Info +- `get-total-users` - Number of users +- `get-user-index` - Get user ID by address +- `get-user-data` - Get user details by ID +- `get-user-data-by-address` - Get user details by address + +#### Invoice Info +- `get-total-invoices` - Number of invoices +- `get-invoice` - Get invoice details +- `get-recent-payment` - Get latest payment ID +- `get-recent-payment-data` - Get latest payment details +- `get-recent-payment-data-by-address` - Get latest payment by user/resource + +#### Contract Info +- `get-payment-address` - Current payment address +- `get-total-revenue` - Total contract revenue +- `get-contract-data` - Aggregate contract statistics + +## Usage Examples + +### Adding a Resource + +```clarity +(contract-call? .aibtc-ext005-payments add-resource + "premium-access" + "Premium API access" + u1000000 + (some "https://api.example.com") +) +``` + +### Making a Payment + +```clarity +(contract-call? .aibtc-ext005-payments pay-invoice-by-resource-name + "premium-access" + (some 0x0102...) +) +``` + +### Checking Payment Status + +```clarity +(contract-call? .aibtc-ext005-payments get-recent-payment-data-by-address + "premium-access" + tx-sender +) +``` diff --git a/docs/dao/extensions/aibtc-ext006-treasury.md b/docs/dao/extensions/aibtc-ext006-treasury.md new file mode 100644 index 0000000..05af612 --- /dev/null +++ b/docs/dao/extensions/aibtc-ext006-treasury.md @@ -0,0 +1,135 @@ +# Treasury Extension + +The treasury extension (`aibtc-ext006-treasury.clar`) manages the DAO's assets including STX, SIP-009 NFTs, and SIP-010 FTs. + +## Key Features + +- Manages multiple asset types: + - STX (native token) + - SIP-010 Fungible Tokens + - SIP-009 Non-Fungible Tokens +- Allowlist-based asset management +- Deposit support for any user +- Protected withdrawals (DAO/extension only) +- STX stacking delegation support + +## Error Codes + +- `ERR_UNAUTHORIZED (6000)` - Caller not authorized +- `ERR_UNKNOWN_ASSSET (6001)` - Asset not on allowlist + +## Functions + +### Asset Management + +#### allow-asset +```clarity +(allow-asset (token principal) (enabled bool)) +``` +Adds or updates an asset on the allowlist. DAO/extension only. + +#### allow-assets +```clarity +(allow-assets (allowList (list 100 {token: principal, enabled: bool}))) +``` +Bulk update of asset allowlist. DAO/extension only. + +### Deposits + +#### deposit-stx +```clarity +(deposit-stx (amount uint)) +``` +Deposits STX to treasury. + +#### deposit-ft +```clarity +(deposit-ft (ft ) (amount uint)) +``` +Deposits SIP-010 tokens to treasury. + +#### deposit-nft +```clarity +(deposit-nft (nft ) (id uint)) +``` +Deposits SIP-009 NFT to treasury. + +### Withdrawals + +#### withdraw-stx +```clarity +(withdraw-stx (amount uint) (recipient principal)) +``` +Withdraws STX from treasury. DAO/extension only. + +#### withdraw-ft +```clarity +(withdraw-ft (ft ) (amount uint) (recipient principal)) +``` +Withdraws SIP-010 tokens from treasury. DAO/extension only. + +#### withdraw-nft +```clarity +(withdraw-nft (nft ) (id uint) (recipient principal)) +``` +Withdraws SIP-009 NFT from treasury. DAO/extension only. + +### Stacking + +#### delegate-stx +```clarity +(delegate-stx (maxAmount uint) (to principal)) +``` +Delegates STX for stacking. DAO/extension only. + +#### revoke-delegate-stx +```clarity +(revoke-delegate-stx) +``` +Revokes STX delegation. DAO/extension only. + +### Read-Only Functions + +#### is-allowed-asset +```clarity +(is-allowed-asset (assetContract principal)) +``` +Returns whether an asset is on allowlist. + +#### get-allowed-asset +```clarity +(get-allowed-asset (assetContract principal)) +``` +Returns allowlist status for an asset. + +## Usage Examples + +### Adding Allowed Asset + +```clarity +(contract-call? .aibtc-ext006-treasury allow-asset .token-contract true) +``` + +### Depositing STX + +```clarity +(contract-call? .aibtc-ext006-treasury deposit-stx u1000000) +``` + +### Depositing FT + +```clarity +(contract-call? .aibtc-ext006-treasury deposit-ft .token-contract u100) +``` + +### Withdrawing NFT + +```clarity +(contract-call? .aibtc-ext006-treasury withdraw-nft .nft-contract u1 tx-sender) +``` + +### Delegating STX + +```clarity +(contract-call? .aibtc-ext006-treasury delegate-stx u1000000 'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR) +``` diff --git a/docs/dao/traits/README.md b/docs/dao/traits/README.md new file mode 100644 index 0000000..a9479cd --- /dev/null +++ b/docs/dao/traits/README.md @@ -0,0 +1,81 @@ +# DAO Traits Documentation + +The aibtcdev DAO uses traits to define interfaces that different components must implement. This ensures consistency and interoperability between components. + +## Core Traits + +### Proposal Trait +```clarity +(define-trait proposal ( + (execute (principal) (response bool uint)) +)) +``` +Base trait that all proposals must implement. Enables execution of governance actions. + +### Extension Trait +```clarity +(define-trait extension ( + (callback (principal (buff 34)) (response bool uint)) +)) +``` +Base trait that all extensions must implement. Enables inter-extension communication. + +### Base DAO Trait +```clarity +(define-trait aibtcdev-base-dao ( + (execute ( principal) (response bool uint)) + (set-extension (principal bool) (response bool uint)) + (request-extension-callback ( (buff 34)) (response bool uint)) +)) +``` +Core DAO functionality for managing proposals and extensions. + +## Extension-Specific Traits + +### Bank Account +Manages STX withdrawals with configurable periods and amounts. +[View Documentation](bank-account.md) + +### Messaging +Enables on-chain messaging capabilities. +[View Documentation](messaging.md) + +### Resources +Manages resource definitions and pricing. +[View Documentation](resources.md) + +### Invoices +Handles payment processing for resources. +[View Documentation](invoices.md) + +### Treasury +Manages multiple asset types including STX, NFTs, and FTs. +[View Documentation](treasury.md) + +### Direct Execute +Enables voting on arbitrary proposals. +[View Documentation](direct-execute.md) + +## Implementation Guidelines + +When implementing these traits: + +1. All functions must return `(response bool uint)` where: + - `bool`: Success/failure indicator + - `uint`: Error code on failure + +2. Error codes should be in designated ranges: + - Base DAO: 900-999 + - Actions: 1000-1999 + - Bank Account: 2000-2999 + - Direct Execute: 3000-3999 + - Messaging: 4000-4999 + - Payments: 5000-5999 + - Treasury: 6000-6999 + +3. Authorization checks should use `is-dao-or-extension` pattern + +4. Events should be logged using consistent format: +```clarity +(print {notification: "event-name", payload: {...}}) +``` diff --git a/docs/dao/traits/bank-account.md b/docs/dao/traits/bank-account.md new file mode 100644 index 0000000..6e2fa26 --- /dev/null +++ b/docs/dao/traits/bank-account.md @@ -0,0 +1,71 @@ +# Bank Account Trait + +The bank account trait (`bank-account`) defines the interface for managing STX withdrawals with configurable periods and amounts. + +## Interface + +```clarity +(define-trait bank-account ( + (set-account-holder (principal) (response bool uint)) + (set-withdrawal-period (uint) (response bool uint)) + (set-withdrawal-amount (uint) (response bool uint)) + (override-last-withdrawal-block (uint) (response bool uint)) + (deposit-stx (uint) (response bool uint)) + (withdraw-stx () (response bool uint)) +)) +``` + +## Functions + +### Configuration + +#### set-account-holder +Sets the principal that can make withdrawals. +- Parameters: + - `principal`: New account holder address +- Access: DAO/extension only +- Returns: Success/failure response + +#### set-withdrawal-period +Sets the required blocks between withdrawals. +- Parameters: + - `uint`: Number of blocks +- Access: DAO/extension only +- Returns: Success/failure response + +#### set-withdrawal-amount +Sets the STX amount per withdrawal. +- Parameters: + - `uint`: Amount in microSTX +- Access: DAO/extension only +- Returns: Success/failure response + +#### override-last-withdrawal-block +Administrative function to override last withdrawal block. +- Parameters: + - `uint`: Block height +- Access: DAO/extension only +- Returns: Success/failure response + +### Operations + +#### deposit-stx +Allows any user to deposit STX. +- Parameters: + - `uint`: Amount in microSTX +- Access: Public +- Returns: Success/failure response + +#### withdraw-stx +Allows account holder to withdraw if period elapsed. +- Parameters: None +- Access: Account holder only +- Returns: Success/failure response + +## Implementation Requirements + +1. Must maintain withdrawal period between withdrawals +2. Must enforce single account holder +3. Must track last withdrawal block +4. Must verify STX transfers succeed +5. Must emit events for all state changes diff --git a/docs/dao/traits/direct-execute.md b/docs/dao/traits/direct-execute.md new file mode 100644 index 0000000..5b9734d --- /dev/null +++ b/docs/dao/traits/direct-execute.md @@ -0,0 +1,109 @@ +# Direct Execute Trait + +The direct execute trait defines the interface for managing and executing arbitrary proposals through token-weighted voting. + +## Interface + +```clarity +(define-trait direct-execute ( + (set-protocol-treasury () (response bool uint)) + (set-voting-token () (response bool uint)) + (create-proposal ( ) (response bool uint)) + (vote-on-proposal ( bool) (response bool uint)) + (conclude-proposal ( ) (response bool uint)) +)) +``` + +## Functions + +### Configuration + +#### set-protocol-treasury +Sets the treasury contract for proposal execution. +- Parameters: + - `treasury`: Treasury contract implementing treasury trait +- Access: DAO/extension only +- Returns: Success/failure response + +#### set-voting-token +Sets the SIP-010 token used for voting. +- Parameters: + - `token`: SIP-010 token contract +- Access: DAO/extension only +- Returns: Success/failure response + +### Proposal Management + +#### create-proposal +Creates new proposal for voting. +- Parameters: + - `proposal`: Proposal contract + - `token`: Voting token contract +- Access: Public +- Returns: Success/failure response + +#### vote-on-proposal +Casts vote on proposal. +- Parameters: + - `proposal`: Proposal contract + - `token`: Voting token contract + - `vote`: True for yes, false for no +- Access: Token holders +- Returns: Success/failure response + +#### conclude-proposal +Concludes voting and executes if passed. +- Parameters: + - `proposal`: Proposal contract + - `treasury`: Treasury contract + - `token`: Voting token contract +- Access: Public +- Returns: Success/failure response + +## Implementation Requirements + +1. Track proposal status and votes +2. Calculate voting power from token balances +3. Enforce voting period +4. Require minimum quorum +5. Execute proposals atomically + +## Usage Pattern + +Implementations should: +1. Verify proposal validity +2. Track voting status +3. Calculate results accurately +4. Handle execution safely + +Example implementation: +```clarity +(define-public (vote-on-proposal + (proposal ) + (token ) + (vote bool) + ) + (let ( + (voting-power (unwrap! (get-balance token tx-sender) ERR_ZERO_POWER)) + ) + ;; Verify voting period + (asserts! (is-voting-active proposal) ERR_VOTING_ENDED) + + ;; Record vote + (try! (record-vote proposal tx-sender vote voting-power)) + + ;; Log vote + (print { + notification: "vote-cast", + payload: { + proposal: proposal, + voter: tx-sender, + power: voting-power, + vote: vote + } + }) + + (ok true) + ) +) +``` diff --git a/docs/dao/traits/extension.md b/docs/dao/traits/extension.md new file mode 100644 index 0000000..2c0f691 --- /dev/null +++ b/docs/dao/traits/extension.md @@ -0,0 +1,57 @@ +# Extension Trait + +The extension trait defines the base interface that all DAO extensions must implement. + +## Interface + +```clarity +(define-trait extension ( + (callback (principal (buff 34)) (response bool uint)) +)) +``` + +## Functions + +### callback +Standard extension callback support. +- Parameters: + - `sender`: Calling principal + - `memo`: Optional callback data (34 bytes) +- Returns: Success/failure response + +## Implementation Requirements + +1. Handle callbacks from DAO and other extensions +2. Process callback memos appropriately +3. Return meaningful success/failure responses +4. Implement authorization checks +5. Log callback events consistently + +## Usage Pattern + +Extensions should implement callback to: +1. Receive notifications from other extensions +2. Process cross-extension requests +3. Handle DAO-initiated actions +4. Support extension-specific callbacks + +Example implementation: +```clarity +(define-public (callback (sender principal) (memo (buff 34))) + (begin + ;; Verify sender is DAO or extension + (try! (is-dao-or-extension)) + + ;; Process callback + (print { + notification: "callback", + payload: { + sender: sender, + memo: memo + } + }) + + (ok true) + ) +) +``` diff --git a/docs/dao/traits/invoices.md b/docs/dao/traits/invoices.md new file mode 100644 index 0000000..843e656 --- /dev/null +++ b/docs/dao/traits/invoices.md @@ -0,0 +1,84 @@ +# Invoices Trait + +The invoices trait defines the interface for processing payments for resources defined through the resources trait. + +## Interface + +```clarity +(define-trait invoices ( + (pay-invoice (uint (optional (buff 34))) (response uint uint)) + (pay-invoice-by-resource-name ((string-utf8 50) (optional (buff 34))) (response uint uint)) +)) +``` + +## Functions + +### Payment Processing + +#### pay-invoice +Processes payment for a resource by ID. +- Parameters: + - `uint`: Resource ID + - `memo`: Optional payment memo (34 bytes) +- Access: Public +- Returns: Invoice ID on success + +#### pay-invoice-by-resource-name +Processes payment for a resource by name. +- Parameters: + - `name`: Resource name + - `memo`: Optional payment memo (34 bytes) +- Access: Public +- Returns: Invoice ID on success + +## Implementation Requirements + +1. Verify resource exists and is enabled +2. Process payment transfer +3. Generate unique invoice ID +4. Track payment status +5. Handle payment memos +6. Emit payment events + +## Usage Pattern + +Implementations should: +1. Validate resource status +2. Process payment atomically +3. Generate sequential invoice IDs +4. Log payment details + +Example implementation: +```clarity +(define-public (pay-invoice-by-resource-name + (name (string-utf8 50)) + (memo (optional (buff 34)))) + (let ( + (resource (unwrap! (get-resource-by-name name) ERR_NOT_FOUND)) + (price (get price resource)) + ) + ;; Verify resource enabled + (asserts! (get enabled resource) ERR_DISABLED) + + ;; Process payment + (try! (stx-transfer? price tx-sender payment-address)) + + ;; Generate invoice + (let ((invoice-id (generate-invoice-id))) + ;; Log payment + (print { + notification: "payment-processed", + payload: { + resource: name, + price: price, + payer: tx-sender, + invoiceId: invoice-id, + memo: memo + } + }) + + (ok invoice-id) + ) + ) +) +``` diff --git a/docs/dao/traits/messaging.md b/docs/dao/traits/messaging.md new file mode 100644 index 0000000..83892d6 --- /dev/null +++ b/docs/dao/traits/messaging.md @@ -0,0 +1,62 @@ +# Messaging Trait + +The messaging trait defines the interface for sending on-chain messages up to 1MB in size. + +## Interface + +```clarity +(define-trait messaging ( + (send ((string-ascii 1048576) bool) (response bool uint)) +)) +``` + +## Functions + +### send +Sends an on-chain message. +- Parameters: + - `message`: ASCII message content (max 1MB) + - `isFromDao`: Whether message is from DAO +- Access: Public for regular messages, DAO/extension only for DAO messages +- Returns: Success/failure response + +## Implementation Requirements + +1. Validate message size (≤ 1MB) +2. Enforce DAO authorization for DAO messages +3. Track message history +4. Handle message encoding +5. Emit message events + +## Usage Pattern + +Implementations should: +1. Verify message size +2. Check authorization for DAO messages +3. Log message details +4. Handle errors gracefully + +Example implementation: +```clarity +(define-public (send (message (string-ascii 1048576)) (isFromDao bool)) + (begin + ;; Check DAO authorization if needed + (if isFromDao + (try! (is-dao-or-extension)) + true + ) + + ;; Log message + (print { + notification: "message-sent", + payload: { + sender: tx-sender, + isFromDao: isFromDao, + message: message + } + }) + + (ok true) + ) +) +``` diff --git a/docs/dao/traits/proposal.md b/docs/dao/traits/proposal.md new file mode 100644 index 0000000..12c7971 --- /dev/null +++ b/docs/dao/traits/proposal.md @@ -0,0 +1,59 @@ +# Proposal Trait + +The proposal trait defines the interface that all DAO proposals must implement. + +## Interface + +```clarity +(define-trait proposal ( + (execute (principal) (response bool uint)) +)) +``` + +## Functions + +### execute +Executes the proposal's changes. +- Parameters: + - `sender`: Principal initiating execution +- Returns: Success/failure response + +## Implementation Requirements + +1. Verify execution authorization +2. Make atomic state changes +3. Return meaningful success/failure +4. Log execution events +5. Handle errors gracefully + +## Usage Pattern + +Proposals should: +1. Implement authorization checks +2. Make targeted state changes +3. Emit comprehensive logs +4. Return clear status + +Example implementation: +```clarity +(define-public (execute (sender principal)) + (begin + ;; Verify sender + (asserts! (is-eq sender .aibtcdev-dao) ERR_UNAUTHORIZED) + + ;; Make changes + (try! (contract-call? .aibtcdev-dao set-something new-value)) + + ;; Log execution + (print { + notification: "proposal-executed", + payload: { + sender: sender, + changes: "..." + } + }) + + (ok true) + ) +) +``` diff --git a/docs/dao/traits/resources.md b/docs/dao/traits/resources.md new file mode 100644 index 0000000..c6f0bdd --- /dev/null +++ b/docs/dao/traits/resources.md @@ -0,0 +1,99 @@ +# Resources Trait + +The resources trait defines the interface for managing payable resources with configurable pricing and descriptions. + +## Interface + +```clarity +(define-trait resources ( + (set-payment-address (principal) (response bool uint)) + (add-resource ((string-utf8 50) (string-utf8 255) uint (optional (string-utf8 255))) (response uint uint)) + (toggle-resource (uint) (response bool uint)) + (toggle-resource-by-name ((string-utf8 50)) (response bool uint)) +)) +``` + +## Functions + +### Configuration + +#### set-payment-address +Sets the payment recipient for resource invoices. +- Parameters: + - `principal`: Payment recipient address +- Access: DAO/extension only +- Returns: Success/failure response + +### Resource Management + +#### add-resource +Creates a new payable resource. +- Parameters: + - `name`: Resource name (max 50 chars) + - `description`: Resource description (max 255 chars) + - `price`: Price in microSTX + - `metadata`: Optional additional data (max 255 chars) +- Access: DAO/extension only +- Returns: Resource ID on success + +#### toggle-resource +Enables/disables a resource by ID. +- Parameters: + - `uint`: Resource ID +- Access: DAO/extension only +- Returns: Success/failure response + +#### toggle-resource-by-name +Enables/disables a resource by name. +- Parameters: + - `name`: Resource name +- Access: DAO/extension only +- Returns: Success/failure response + +## Implementation Requirements + +1. Maintain unique resource names +2. Track resource enabled/disabled status +3. Validate input parameters +4. Enforce DAO/extension-only access +5. Emit events for state changes + +## Usage Pattern + +Implementations should: +1. Verify resource name uniqueness +2. Validate price > 0 +3. Track resource status +4. Log all changes + +Example implementation: +```clarity +(define-public (add-resource (name (string-utf8 50)) + (description (string-utf8 255)) + (price uint) + (metadata (optional (string-utf8 255)))) + (begin + ;; Check authorization + (try! (is-dao-or-extension)) + + ;; Validate inputs + (asserts! (> price u0) ERR_INVALID_PRICE) + (asserts! (not (resource-exists name)) ERR_NAME_EXISTS) + + ;; Save resource + (try! (save-resource name description price metadata)) + + ;; Log creation + (print { + notification: "resource-added", + payload: { + name: name, + price: price, + description: description + } + }) + + (ok true) + ) +) +``` diff --git a/docs/dao/traits/treasury.md b/docs/dao/traits/treasury.md new file mode 100644 index 0000000..64a6adb --- /dev/null +++ b/docs/dao/traits/treasury.md @@ -0,0 +1,142 @@ +# Treasury Trait + +The treasury trait defines the interface for managing multiple asset types including STX, SIP-009 NFTs, and SIP-010 FTs. + +## Interface + +```clarity +(define-trait treasury ( + (allow-asset (principal bool) (response bool uint)) + (deposit-stx (uint) (response bool uint)) + (deposit-ft ( uint) (response bool uint)) + (deposit-nft ( uint) (response bool uint)) + (withdraw-stx (uint principal) (response bool uint)) + (withdraw-ft ( uint principal) (response bool uint)) + (withdraw-nft ( uint principal) (response bool uint)) + (delegate-stx (uint principal) (response bool uint)) + (revoke-delegate-stx () (response bool uint)) +)) +``` + +## Functions + +### Asset Management + +#### allow-asset +Controls which assets can be deposited/withdrawn. +- Parameters: + - `token`: Asset contract principal + - `enabled`: Whether to allow the asset +- Access: DAO/extension only +- Returns: Success/failure response + +### Deposits + +#### deposit-stx +Deposits STX to treasury. +- Parameters: + - `amount`: Amount in microSTX +- Access: Public +- Returns: Success/failure response + +#### deposit-ft +Deposits SIP-010 tokens. +- Parameters: + - `ft`: Token contract + - `amount`: Token amount +- Access: Public +- Returns: Success/failure response + +#### deposit-nft +Deposits SIP-009 NFT. +- Parameters: + - `nft`: NFT contract + - `id`: Token ID +- Access: Public +- Returns: Success/failure response + +### Withdrawals + +#### withdraw-stx +Withdraws STX from treasury. +- Parameters: + - `amount`: Amount in microSTX + - `recipient`: Recipient address +- Access: DAO/extension only +- Returns: Success/failure response + +#### withdraw-ft +Withdraws SIP-010 tokens. +- Parameters: + - `ft`: Token contract + - `amount`: Token amount + - `recipient`: Recipient address +- Access: DAO/extension only +- Returns: Success/failure response + +#### withdraw-nft +Withdraws SIP-009 NFT. +- Parameters: + - `nft`: NFT contract + - `id`: Token ID + - `recipient`: Recipient address +- Access: DAO/extension only +- Returns: Success/failure response + +### Stacking + +#### delegate-stx +Delegates STX for PoX stacking. +- Parameters: + - `amount`: Max amount to delegate + - `to`: Delegate address +- Access: DAO/extension only +- Returns: Success/failure response + +#### revoke-delegate-stx +Revokes STX delegation. +- Parameters: None +- Access: DAO/extension only +- Returns: Success/failure response + +## Implementation Requirements + +1. Maintain allowlist of accepted assets +2. Verify asset transfers succeed +3. Enforce DAO/extension-only access for protected functions +4. Track asset balances accurately +5. Handle PoX delegation safely + +## Usage Pattern + +Implementations should: +1. Use allowlist for asset control +2. Verify transfer success +3. Emit comprehensive logs +4. Handle errors gracefully + +Example implementation: +```clarity +(define-public (deposit-ft (ft ) (amount uint)) + (begin + ;; Check allowlist + (asserts! (is-allowed-asset (contract-of ft)) ERR_UNKNOWN_ASSET) + + ;; Transfer tokens + (try! (contract-call? ft transfer + amount tx-sender TREASURY none)) + + ;; Log deposit + (print { + notification: "deposit-ft", + payload: { + token: (contract-of ft), + amount: amount, + sender: tx-sender + } + }) + + (ok true) + ) +) +``` diff --git a/docs/smart-contract-test-plan.md b/docs/smart-contract-test-plan.md new file mode 100644 index 0000000..ec5cbad --- /dev/null +++ b/docs/smart-contract-test-plan.md @@ -0,0 +1,179 @@ +# aibtc smart contract test plan + +All tests will be created with and executed using the Clarinet JS SDK. + +## Main DAO + +### aibtcdev-base-dao + +set-extension() fails if caller is not DAO or extension +set-extensions() fails if caller is not DAO or extension +execute() fails if caller is not DAO or extension +construct() fails when called by an account that is not the deployer +construct() fails when initializing the DAO with bootstrap proposal a second time +construct() succeeds when initializing the DAO with bootstrap proposal +request-extension-callback() fails if caller is not an extension +request-extension-callback() succeeds and calls an extension + +is-extension() succeeds and returns false with unrecognized extension +is-extension() succeeds and returns true for active extensions +executed-at() succeeds and returns none with unrecognized proposal +executed-at() succeeds and returns the Bitcoin block height the proposal was executed + +## Extensions + +### aibtc-ext001-actions + +propose-action() fails if contract not initialized +propose-action() fails if token mismatches +propose-action() fails if caller has no balance +propose-action() succeeds and creates new proposal + +vote-on-proposal() fails if contract not initialized +vote-on-proposal() fails if token mismatches +vote-on-proposal() fails if caller has no balance +vote-on-proposal() fails if voting too soon +vote-on-proposal() fails if voting too late +vote-on-proposal() fails if proposal concluded +vote-on-proposal() fails if already voted +vote-on-proposal() succeeds and records vote + +conclude-proposal() fails if contract not initialized +conclude-proposal() fails if treasury mismatches +conclude-proposal() fails if proposal still active +conclude-proposal() fails if proposal already concluded +conclude-proposal() succeeds and executes if passed +conclude-proposal() succeeds without executing if failed + +set-protocol-treasury() fails if caller is not DAO or extension +set-protocol-treasury() fails if treasury is not a contract +set-protocol-treasury() fails if treasury is self +set-protocol-treasury() fails if treasury is already set +set-protocol-treasury() succeeds and sets new treasury + +set-voting-token() fails if caller is not DAO or extension +set-voting-token() fails if token is not a contract +set-voting-token() fails if token is not initialized +set-voting-token() fails if token mismatches +set-voting-token() succeeds and sets new token + + +### aibtc-ext003-direct-execute + +set-protocol-treasury() fails if caller is not DAO or extension +set-protocol-treasury() fails if treasury is not a contract +set-protocol-treasury() fails if treasury is self +set-protocol-treasury() fails if treasury is already set +set-protocol-treasury() succeeds and sets new treasury + +set-voting-token() fails if caller is not DAO or extension +set-voting-token() fails if token is not a contract +set-voting-token() fails if token is not initialized +set-voting-token() fails if token mismatches +set-voting-token() succeeds and sets new token + +create-proposal() fails if contract not initialized +create-proposal() fails if token mismatches +create-proposal() fails if caller has no balance +create-proposal() fails if proposal already executed +create-proposal() succeeds and creates new proposal + +vote-on-proposal() fails if contract not initialized +vote-on-proposal() fails if token mismatches +vote-on-proposal() fails if caller has no balance +vote-on-proposal() fails if proposal already executed +vote-on-proposal() fails if voting too soon +vote-on-proposal() fails if voting too late +vote-on-proposal() fails if proposal concluded +vote-on-proposal() fails if already voted +vote-on-proposal() succeeds and records vote + +conclude-proposal() fails if contract not initialized +conclude-proposal() fails if treasury mismatches +conclude-proposal() fails if proposal already executed +conclude-proposal() fails if proposal still active +conclude-proposal() fails if proposal already concluded +conclude-proposal() succeeds and executes if passed +conclude-proposal() succeeds without executing if failed + +### aibtc-ext004-messaging + +send() succeeds if called by any user with isFromDao false +send() fails if called by any user with isFromDao true +send() succeeds if called by a DAO proposal with isFromDao true + +### aibtc-ext005-payments + +set-payment-address() fails if caller is not DAO or extension +set-payment-address() fails if old address matches current payment address +set-payment-address() fails if old address and new address are the same +set-payment-address() succeeds and sets the new payment address + +add-resource() fails if caller is not DAO or extension +add-resource() fails if name is blank +add-resource() fails if description is blank +add-resource() fails if price is 0 +add-resource() fails if provided url is blank +add-resource() fails if resource name already used +add-resource() succeeds and adds a new resource + +toggle-resource() fails if caller is not DAO or extension +toggle-resource() fails if resource is not found +toggle-resource() fails if resource index is 0 +toggle-resource() succeeds and toggles if resource is enabled + +toggle-resource-by-name() fails if caller is not DAO or extension +toggle-resource-by-name() fails if resource is not found +toggle-resource() succeeds and toggles if resource is enabled + +pay-invoice() fails if resource is not found +pay-invoice() fails if resource index is 0 +pay-invoice() fails if resource is disabled +pay-invoice() succeeds and updates info for resource + +pay-invoice-by-resource-name() fails if resource is not found +pay-invoice-by-resource-name() fails if resource is disabled +pay-invoice-by-resource-name() succeeds and updates info for resource + +### aibtc-ext006-treasury + +allow-asset() fails if caller is not DAO or extension +allow-asset() succeeds and sets new allowed asset +allow-asset() succeeds and toggles status of existing asset + +allow-assets() fails if caller is not DAO or extension +allow-assets() succeeds and sets new allowed assets +allow-assets() succeeds and toggles status of existing assets + +deposit-stx() succeeds and deposits STX to the treasury + +deposit-ft() fails if asset is not allowed +deposit-ft() succeeds and transfers FT to treasury + +deposit-nft() fails if asset is not allowed +deposit-nft() succeeds and transfers NFT to treasury + +withdraw-stx() fails if caller is not DAO or extension +withdraw-stx() succeeds and transfers STX to a standard principal +withdraw-stx() succeeds and transfers STX to a contract principal + +withdraw-ft() fails if caller is not DAO or extension +withdraw-ft() succeeds and transfers FT to a standard principal +withdraw-ft() succeeds and transfers FT to a contract principal + +withdraw-nft() fails if caller is not DAO or extension +withdraw-nft() succeeds and transfers NFT to a standard principal +withdraw-nft() succeeds and transfers NFT to a contract principal + +delegate-stx() fails if caller is not DAO or extension +delegate-stx() succeeds and delegates to Stacks PoX + +revoke-delegate-stx() fails if caller is not DAO or extension +revoke-delegate-stx() fails if contract is not currently stacking +revoke-delegate-stx() succeeds and revokes stacking delegation + +## Proposals + +### aibtc-prop001-bootstrap + +get-dao-manifest() returns DAO_MANIFEST as string diff --git a/tests/aibtcdev-aibtc.test.ts b/tests/aibtcdev-aibtc.test.ts deleted file mode 100644 index 3c7b051..0000000 --- a/tests/aibtcdev-aibtc.test.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { Cl } from "@stacks/transactions"; -import { describe, expect, it } from "vitest"; - -enum ErrCode { - ERR_NOT_AUTHORIZED = 1000, -} - -const FAUCET_DRIP = 10_000; // 0.0001 BTC -const FAUCET_DROP = 1_000_000; // 0.01 BTC -const FAUCET_FLOOD = 100_000_000; // 1 BTC - -describe("aibtcdev-aibtc", () => { - // faucet drip - it(`faucet-drip(): succeeds and mints ${FAUCET_DRIP} aiBTC`, () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const address1 = accounts.get("wallet_1")!; - // ACT - const response = simnet.callPublicFn( - "aibtcdev-aibtc", - "faucet-drip", - [Cl.principal(address1)], - address1 - ); - const balance = simnet.callReadOnlyFn( - "aibtcdev-aibtc", - "get-balance", - [Cl.principal(address1)], - address1 - ); - // ASSERT - expect(response.result).toBeOk(Cl.bool(true)); - expect(balance.result).toBeOk(Cl.uint(FAUCET_DRIP)); - }); - // faucet drop - it(`faucet-drop(): succeeds and mints ${FAUCET_DROP} aiBTC`, () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const address1 = accounts.get("wallet_1")!; - // ACT - const response = simnet.callPublicFn( - "aibtcdev-aibtc", - "faucet-drop", - [Cl.principal(address1)], - address1 - ); - const balance = simnet.callReadOnlyFn( - "aibtcdev-aibtc", - "get-balance", - [Cl.principal(address1)], - address1 - ); - // ASSERT - expect(response.result).toBeOk(Cl.bool(true)); - expect(balance.result).toBeOk(Cl.uint(FAUCET_DROP)); - }); - // faucet flood - it(`faucet-flood(): succeeds and mints ${FAUCET_FLOOD} aiBTC`, () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const address1 = accounts.get("wallet_1")!; - // ACT - const response = simnet.callPublicFn( - "aibtcdev-aibtc", - "faucet-flood", - [Cl.principal(address1)], - address1 - ); - const balance = simnet.callReadOnlyFn( - "aibtcdev-aibtc", - "get-balance", - [Cl.principal(address1)], - address1 - ); - // ASSERT - expect(response.result).toBeOk(Cl.bool(true)); - expect(balance.result).toBeOk(Cl.uint(FAUCET_FLOOD)); - }); - // transfer works - it("transfer(): succeeds and transfers aiBTC between accounts", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const address1 = accounts.get("wallet_1")!; - const address2 = accounts.get("wallet_2")!; - const address3 = accounts.get("wallet_3")!; - - const funding = [ - simnet.callPublicFn( - "aibtcdev-aibtc", - "faucet-flood", - [Cl.principal(address1)], - address1 - ), - simnet.callPublicFn( - "aibtcdev-aibtc", - "faucet-flood", - [Cl.principal(address2)], - address2 - ), - simnet.callPublicFn( - "aibtcdev-aibtc", - "faucet-flood", - [Cl.principal(address3)], - address3 - ), - ]; - // ACT - - // xfer from 1 to 2 - const transfer1 = simnet.callPublicFn( - "aibtcdev-aibtc", - "transfer", - [ - Cl.uint(FAUCET_DRIP), - Cl.principal(address1), - Cl.principal(address2), - Cl.none(), - ], - address1 - ); - // xfer from 2 to 3 - const transfer2 = simnet.callPublicFn( - "aibtcdev-aibtc", - "transfer", - [ - Cl.uint(FAUCET_DROP), - Cl.principal(address2), - Cl.principal(address3), - Cl.none(), - ], - address2 - ); - // xfer from 3 to 1 - const transfer3 = simnet.callPublicFn( - "aibtcdev-aibtc", - "transfer", - [ - Cl.uint(FAUCET_FLOOD), - Cl.principal(address3), - Cl.principal(address1), - Cl.none(), - ], - address3 - ); - - // get balances - const balance1 = simnet.callReadOnlyFn( - "aibtcdev-aibtc", - "get-balance", - [Cl.principal(address1)], - address1 - ); - const balance2 = simnet.callReadOnlyFn( - "aibtcdev-aibtc", - "get-balance", - [Cl.principal(address2)], - address1 - ); - const balance3 = simnet.callReadOnlyFn( - "aibtcdev-aibtc", - "get-balance", - [Cl.principal(address3)], - address1 - ); - - // ASSERT - for (const response of funding) { - expect(response.result).toBeOk(Cl.bool(true)); - } - expect(transfer1.result).toBeOk(Cl.bool(true)); - expect(transfer2.result).toBeOk(Cl.bool(true)); - expect(transfer3.result).toBeOk(Cl.bool(true)); - // Minted - Sent Amount + Received Amount - expect(balance1.result).toBeOk( - Cl.uint(FAUCET_FLOOD - FAUCET_DRIP + FAUCET_FLOOD) - ); - expect(balance2.result).toBeOk( - Cl.uint(FAUCET_FLOOD - FAUCET_DROP + FAUCET_DRIP) - ); - expect(balance3.result).toBeOk( - Cl.uint(FAUCET_FLOOD - FAUCET_FLOOD + FAUCET_DROP) - ); - }); -}); diff --git a/tests/aibtcdev-bank-account.test.ts b/tests/aibtcdev-bank-account.test.ts deleted file mode 100644 index 6577c7c..0000000 --- a/tests/aibtcdev-bank-account.test.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { Cl } from "@stacks/transactions"; -import { describe, expect, it } from "vitest"; - -const accounts = simnet.getAccounts(); -const address1 = accounts.get("wallet_1")!; -const address2 = accounts.get("wallet_2")!; -const addressDeployer = accounts.get("deployer")!; - -const contractAddress = `${addressDeployer}.aibtcdev-bank-account`; - -enum ErrCode { - ERR_INVALID = 1000, - ERR_UNAUTHORIZED = 1001, - ERR_TOO_SOON = 1002, -} - -const withdrawalAmount = 10000000; -const withdrawalPeriod = 144; - -describe("aibtcdev-bank-account", () => { - describe("set-account-holder", () => { - it("succeeds when deployer sets a valid account holder", () => { - const response = simnet.callPublicFn( - contractAddress, - "set-account-holder", - [Cl.principal(address1)], - addressDeployer - ); - expect(response.result).toBeOk(Cl.bool(true)); - }); - - it("succeeds when deployer sets a valid account holder a second time", () => { - simnet.callPublicFn( - contractAddress, - "set-account-holder", - [Cl.principal(address1)], - addressDeployer - ); - const response = simnet.callPublicFn( - contractAddress, - "set-account-holder", - [Cl.principal(address2)], - addressDeployer - ); - expect(response.result).toBeOk(Cl.bool(true)); - }); - - it("fails when a non-deployer tries to set the account holder", () => { - const response = simnet.callPublicFn( - contractAddress, - "set-account-holder", - [Cl.principal(address1)], - address1 - ); - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_UNAUTHORIZED)); - }); - - it("fails if the deployer tries to set an invalid account holder", () => { - const response = simnet.callPublicFn( - contractAddress, - "set-account-holder", - [Cl.principal(contractAddress)], - addressDeployer - ); - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_INVALID)); - }); - }); - - describe("set-withdrawal-period", () => { - it("succeeds when deployer sets a valid period", () => { - const response = simnet.callPublicFn( - contractAddress, - "set-withdrawal-period", - [Cl.uint(200)], - addressDeployer - ); - expect(response.result).toBeOk(Cl.bool(true)); - }); - - it("fails when a non-deployer tries to set the period", () => { - const response = simnet.callPublicFn( - contractAddress, - "set-withdrawal-period", - [Cl.uint(200)], - address1 - ); - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_UNAUTHORIZED)); - }); - - it("fails when deployer sets an invalid period", () => { - const response = simnet.callPublicFn( - contractAddress, - "set-withdrawal-period", - [Cl.uint(0)], - addressDeployer - ); - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_INVALID)); - }); - }); - - describe("set-withdrawal-amount", () => { - it("succeeds when deployer sets a valid amount", () => { - const response = simnet.callPublicFn( - contractAddress, - "set-withdrawal-amount", - [Cl.uint(15000000)], - addressDeployer - ); - expect(response.result).toBeOk(Cl.bool(true)); - }); - - it("fails when a non-deployer tries to set the amount", () => { - const response = simnet.callPublicFn( - contractAddress, - "set-withdrawal-amount", - [Cl.uint(15000000)], - address1 - ); - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_UNAUTHORIZED)); - }); - - it("fails when deployer sets an invalid amount", () => { - const response = simnet.callPublicFn( - contractAddress, - "set-withdrawal-amount", - [Cl.uint(0)], - addressDeployer - ); - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_INVALID)); - }); - }); - - describe("override-last-withdrawal-block", () => { - it("succeeds when deployer sets a valid block height", () => { - const response = simnet.callPublicFn( - contractAddress, - "override-last-withdrawal-block", - [Cl.uint(500)], - addressDeployer - ); - expect(response.result).toBeOk(Cl.bool(true)); - }); - - it("fails when a non-deployer tries to set the block height", () => { - const response = simnet.callPublicFn( - contractAddress, - "override-last-withdrawal-block", - [Cl.uint(500)], - address1 - ); - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_UNAUTHORIZED)); - }); - - it("fails when deployer sets an invalid block height", () => { - const response = simnet.callPublicFn( - contractAddress, - "override-last-withdrawal-block", - [Cl.uint(0)], - addressDeployer - ); - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_INVALID)); - }); - }); - - describe("deposit-stx", () => { - it("succeeds when user deposits STX into the contract", () => { - const response = simnet.callPublicFn( - contractAddress, - "deposit-stx", - [Cl.uint(10000000)], - address1 - ); - expect(response.result).toBeOk(Cl.bool(true)); - }); - }); - - describe("withdraw-stx", () => { - it("succeeds when an authorized user withdraws STX", () => { - // arrange - simnet.callPublicFn( - contractAddress, - "deposit-stx", - [Cl.uint(100000000)], - address1 - ); - simnet.callPublicFn( - contractAddress, - "set-account-holder", - [Cl.principal(address1)], - addressDeployer - ); - simnet.mineEmptyBlocks(200); - const response = simnet.callPublicFn( - contractAddress, - "withdraw-stx", - [], - address1 - ); - expect(response.result).toBeOk(Cl.bool(true)); - }); - - it("fails when a non-authorized user tries to withdraw", () => { - const response = simnet.callPublicFn( - contractAddress, - "withdraw-stx", - [], - address2 - ); - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_UNAUTHORIZED)); - }); - - it("fails when the user tries to withdraw too soon", () => { - simnet.callPublicFn( - contractAddress, - "set-account-holder", - [Cl.principal(address1)], - addressDeployer - ); - simnet.callPublicFn( - contractAddress, - "override-last-withdrawal-block", - [Cl.uint(simnet.blockHeight)], - addressDeployer - ); - const response = simnet.callPublicFn( - contractAddress, - "withdraw-stx", - [], - address1 - ); - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_TOO_SOON)); - }); - }); - - describe("get-account-balance", () => { - it("succeeds and returns the contract balance", () => { - const response = simnet.callReadOnlyFn( - contractAddress, - "get-account-balance", - [], - addressDeployer - ); - expect(response.result).toBeUint(0); - }); - - it("succeeds and returns the contract balance after deposit", () => { - simnet.callPublicFn( - contractAddress, - "deposit-stx", - [Cl.uint(100000000)], - address1 - ); - const response = simnet.callReadOnlyFn( - contractAddress, - "get-account-balance", - [], - addressDeployer - ); - expect(response.result).toBeUint(100000000); - }); - }); - - describe("get-withdrawal-period", () => { - it("succeeds and returns the withdrawal period", () => { - const response = simnet.callReadOnlyFn( - contractAddress, - "get-withdrawal-period", - [], - address1 - ); - expect(response.result).toBeUint(withdrawalPeriod); - }); - }); - - describe("get-withdrawal-amount", () => { - it("succeeds and returns the withdrawal amount", () => { - const response = simnet.callReadOnlyFn( - contractAddress, - "get-withdrawal-amount", - [], - address1 - ); - expect(response.result).toBeUint(withdrawalAmount); - }); - }); - - describe("get-last-withdrawal-block", () => { - it("succeeds and returns the last withdrawal block", () => { - const response = simnet.callReadOnlyFn( - contractAddress, - "get-last-withdrawal-block", - [], - address1 - ); - expect(response.result).toBeUint(0); - }); - }); - - describe("get-all-vars", () => { - it("succeeds and returns all the variables", () => { - const expectedResponse = { - withdrawalPeriod: Cl.uint(withdrawalPeriod), - withdrawalAmount: Cl.uint(withdrawalAmount), - lastWithdrawalBlock: Cl.uint(0), - }; - - const response = simnet.callReadOnlyFn( - contractAddress, - "get-all-vars", - [], - address1 - ).result; - - expect(response).toBeTuple(expectedResponse); - }); - }); - - describe("get-standard-caller", () => { - it("succeeds and returns the caller", () => { - const response = simnet.callReadOnlyFn( - contractAddress, - "get-standard-caller", - [], - address1 - ); - expect(response.result).toBePrincipal(address1); - }); - - it("succeeds and returns the caller with a proxy", () => { - const response = simnet.callPublicFn( - "test-proxy", - "get-standard-caller", - [], - address1 - ); - expect(response.result).toBeOk(Cl.principal(addressDeployer)); - }); - }); -}); diff --git a/tests/aibtcdev-messaging.test.ts b/tests/aibtcdev-messaging.test.ts deleted file mode 100644 index 2b176e2..0000000 --- a/tests/aibtcdev-messaging.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Cl, cvToValue } from "@stacks/transactions"; -import { describe, expect, it } from "vitest"; - -const accounts = simnet.getAccounts(); -const address1 = accounts.get("wallet_1")!; - -type MessageEnvelope = { - caller: string; - height: number; - sender: string; -}; - -describe("aibtcdev-messaging", () => { - const message = "Hello, world!"; - it("prints the envelope and message when called", () => { - const response = simnet.callPublicFn( - "aibtcdev-messaging", - "send", - [Cl.stringAscii(message)], - address1 - ); - - // first event should be the envelope - const expectedEnvelope: MessageEnvelope = { - caller: address1, - height: simnet.blockHeight, - sender: address1, - }; - - const envelopeEvent = cvToValue(response.events[0].data.value!); - const actualEnvelope = { - caller: envelopeEvent.caller.value, - height: Number(envelopeEvent.height.value), - sender: envelopeEvent.sender.value, - }; - expect(actualEnvelope).toEqual(expectedEnvelope); - - expect(response.result).toBeOk(Cl.bool(true)); - }); -}); diff --git a/tests/aibtcdev-resources-v1.test.ts b/tests/aibtcdev-resources-v1.test.ts deleted file mode 100644 index 92d7cc4..0000000 --- a/tests/aibtcdev-resources-v1.test.ts +++ /dev/null @@ -1,850 +0,0 @@ -import { Cl, cvToValue } from "@stacks/transactions"; -import { describe, expect, it } from "vitest"; - -enum ErrCode { - ERR_UNAUTHORIZED = 1000, - ERR_INVALID_PARAMS, - ERR_NAME_ALREADY_USED, - ERR_SAVING_RESOURCE_DATA, - ERR_DELETING_RESOURCE_DATA, - ERR_RESOURCE_NOT_FOUND, - ERR_RESOURCE_NOT_ENABLED, - ERR_USER_ALREADY_EXISTS, - ERR_SAVING_USER_DATA, - ERR_USER_NOT_FOUND, - ERR_INVOICE_ALREADY_PAID, - ERR_SAVING_INVOICE_DATA, -} - -const createResource = (name: string, desc: string, price: number) => { - return [Cl.stringUtf8(name), Cl.stringUtf8(desc), Cl.uint(price)]; -}; - -const defaultPrice = 10_000; // 0.0001 aiBTC - -const testResource = [ - Cl.stringUtf8("Bitcoin Face"), - Cl.stringUtf8("Generate a unique Bitcoin face."), - Cl.uint(defaultPrice), -]; - -describe("Adding a resource", () => { - it("add-resource() fails if not called by deployer", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const address1 = accounts.get("wallet_1")!; - // ACT - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "add-resource", - testResource, - address1 - ); - // ASSERT - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_UNAUTHORIZED)); - }); - - it("add-resource() fails if name is blank", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const deployer = accounts.get("deployer")!; - // ACT - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "add-resource", - createResource("", "description", defaultPrice), - deployer - ); - // ASSERT - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_INVALID_PARAMS)); - }); - - it("add-resource() fails if description is blank", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const deployer = accounts.get("deployer")!; - // ACT - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "add-resource", - createResource("name", "", defaultPrice), - deployer - ); - // ASSERT - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_INVALID_PARAMS)); - }); - - it("add-resource() fails if price is 0", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const deployer = accounts.get("deployer")!; - // ACT - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "add-resource", - createResource("name", "description", 0), - deployer - ); - // ASSERT - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_INVALID_PARAMS)); - }); - it("add-resource() fails if name already used", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const deployer = accounts.get("deployer")!; - const expectedCount = 1; - // ACT - const firstResponse = simnet.callPublicFn( - "aibtcdev-resources-v1", - "add-resource", - testResource, - deployer - ); - const secondResponse = simnet.callPublicFn( - "aibtcdev-resources-v1", - "add-resource", - testResource, - deployer - ); - // ASSERT - expect(firstResponse.result).toBeOk(Cl.uint(expectedCount)); - expect(secondResponse.result).toBeErr( - Cl.uint(ErrCode.ERR_NAME_ALREADY_USED) - ); - }); - - it("add-resource() succeeds and increments resource count", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const deployer = accounts.get("deployer")!; - const expectedCount = 1; - // ACT - const oldCount = simnet.callReadOnlyFn( - "aibtcdev-resources-v1", - "get-total-resources", - [], - deployer - ); - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "add-resource", - testResource, - deployer - ); - const newCount = simnet.callReadOnlyFn( - "aibtcdev-resources-v1", - "get-total-resources", - [], - deployer - ); - // ASSERT - expect(oldCount.result).toBeUint(expectedCount - 1); - expect(response.result).toBeOk(Cl.uint(expectedCount)); - expect(newCount.result).toBeUint(expectedCount); - }); -}); - -describe("Toggling a Resource Status", () => { - it("toggle-resource() fails if not called by deployer", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const deployer = accounts.get("deployer")!; - const address1 = accounts.get("wallet_1")!; - // ACT - // create resource - simnet.callPublicFn( - "aibtcdev-resources-v1", - "add-resource", - testResource, - deployer - ); - // progress the chain - simnet.mineEmptyBlocks(5000); - // toggle resource - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "toggle-resource", - [Cl.uint(1)], - address1 - ); - // ASSERT - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_UNAUTHORIZED)); - }); - - it("toggle-resource() fails if provided index is greater than current resource count", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const deployer = accounts.get("deployer")!; - // ACT - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "toggle-resource", - [Cl.uint(10)], - deployer - ); - // ASSERT - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_RESOURCE_NOT_FOUND)); - }); - - it("toggle-resource() succeeds and toggles resource status", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const deployer = accounts.get("deployer")!; - // ACT - // create resource - simnet.callPublicFn( - "aibtcdev-resources-v1", - "add-resource", - testResource, - deployer - ); - const resourceBlock = simnet.blockHeight; - // progress the chain - simnet.mineEmptyBlocks(5000); - // toggle resource - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "toggle-resource", - [Cl.uint(1)], - deployer - ); - // get resource - const resourceResponse = simnet.callReadOnlyFn( - "aibtcdev-resources-v1", - "get-resource", - [Cl.uint(1)], - deployer - ); - // progress the chain - simnet.mineEmptyBlocks(5000); - // toggle resource - const response2 = simnet.callPublicFn( - "aibtcdev-resources-v1", - "toggle-resource", - [Cl.uint(1)], - deployer - ); - // get resource - const resourceResponse2 = simnet.callReadOnlyFn( - "aibtcdev-resources-v1", - "get-resource", - [Cl.uint(1)], - deployer - ); - - // ASSERT - expect(response.result).toBeOk(Cl.bool(false)); - expect(resourceResponse.result).toBeSome( - Cl.tuple({ - createdAt: Cl.uint(resourceBlock), - enabled: Cl.bool(false), - description: Cl.stringUtf8("Generate a unique Bitcoin face."), - name: Cl.stringUtf8("Bitcoin Face"), - price: Cl.uint(defaultPrice), - totalSpent: Cl.uint(0), - totalUsed: Cl.uint(0), - }) - ); - expect(response2.result).toBeOk(Cl.bool(true)); - expect(resourceResponse2.result).toBeSome( - Cl.tuple({ - createdAt: Cl.uint(resourceBlock), - enabled: Cl.bool(true), - description: Cl.stringUtf8("Generate a unique Bitcoin face."), - name: Cl.stringUtf8("Bitcoin Face"), - price: Cl.uint(defaultPrice), - totalSpent: Cl.uint(0), - totalUsed: Cl.uint(0), - }) - ); - }); - - it("toggle-resource-by-name(): fails if not called by deployer", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const deployer = accounts.get("deployer")!; - const address1 = accounts.get("wallet_1")!; - // ACT - // create resource - simnet.callPublicFn( - "aibtcdev-resources-v1", - "add-resource", - testResource, - deployer - ); - // progress the chain - simnet.mineEmptyBlocks(5000); - // toggle resource - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "toggle-resource-by-name", - [Cl.stringUtf8("Bitcoin Face")], - address1 - ); - // ASSERT - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_UNAUTHORIZED)); - }); - - it("toggle-resource-by-name() fails if provided name is not found", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const deployer = accounts.get("deployer")!; - // ACT - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "toggle-resource-by-name", - [Cl.stringUtf8("Nothingburger")], - deployer - ); - // ASSERT - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_RESOURCE_NOT_FOUND)); - }); - - it("toggle-resource-by-name() succeeds and toggles resource status", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const deployer = accounts.get("deployer")!; - // ACT - // create resource - simnet.callPublicFn( - "aibtcdev-resources-v1", - "add-resource", - testResource, - deployer - ); - const resourceBlock = simnet.blockHeight; - // progress the chain - simnet.mineEmptyBlocks(5000); - // toggle resource - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "toggle-resource-by-name", - [Cl.stringUtf8("Bitcoin Face")], - deployer - ); - // get resource - const resourceResponse = simnet.callReadOnlyFn( - "aibtcdev-resources-v1", - "get-resource", - [Cl.uint(1)], - deployer - ); - // progress the chain - simnet.mineEmptyBlocks(5000); - // toggle resource - const response2 = simnet.callPublicFn( - "aibtcdev-resources-v1", - "toggle-resource-by-name", - [Cl.stringUtf8("Bitcoin Face")], - deployer - ); - // get resource - const resourceResponse2 = simnet.callReadOnlyFn( - "aibtcdev-resources-v1", - "get-resource", - [Cl.uint(1)], - deployer - ); - - // ASSERT - expect(response.result).toBeOk(Cl.bool(false)); - expect(resourceResponse.result).toBeSome( - Cl.tuple({ - createdAt: Cl.uint(resourceBlock), - enabled: Cl.bool(false), - description: Cl.stringUtf8("Generate a unique Bitcoin face."), - name: Cl.stringUtf8("Bitcoin Face"), - price: Cl.uint(defaultPrice), - totalSpent: Cl.uint(0), - totalUsed: Cl.uint(0), - }) - ); - expect(response2.result).toBeOk(Cl.bool(true)); - expect(resourceResponse2.result).toBeSome( - Cl.tuple({ - createdAt: Cl.uint(resourceBlock), - enabled: Cl.bool(true), - description: Cl.stringUtf8("Generate a unique Bitcoin face."), - name: Cl.stringUtf8("Bitcoin Face"), - price: Cl.uint(defaultPrice), - totalSpent: Cl.uint(0), - totalUsed: Cl.uint(0), - }) - ); - }); -}); - -describe("Setting a Payment Address", () => { - it("set-payment-address() fails if not called by deployer", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const address1 = accounts.get("wallet_1")!; - // ACT - // get current payment address - const currentPaymentAddressResponse = simnet.callReadOnlyFn( - "aibtcdev-resources-v1", - "get-payment-address", - [], - address1 - ); - // parse into an object we can read - const currentPaymentAddress = cvToValue( - currentPaymentAddressResponse.result - ); - // set payment address - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "set-payment-address", - [ - Cl.standardPrincipal(currentPaymentAddress.value), - Cl.standardPrincipal(address1), - ], - address1 - ); - // ASSERT - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_UNAUTHORIZED)); - }); - - it("set-payment-address() fails if old address param is incorrect", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const address1 = accounts.get("wallet_1")!; - const deployer = accounts.get("deployer")!; - // ACT - // set payment address - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "set-payment-address", - [Cl.standardPrincipal(address1), Cl.standardPrincipal(address1)], - deployer - ); - // ASSERT - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_UNAUTHORIZED)); - }); - - it("set-payment-address() succeeds if called by the deployer", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const deployer = accounts.get("deployer")!; - const address1 = accounts.get("wallet_1")!; - - // ACT - // get current payment address - const currentPaymentAddressResponse = simnet.callReadOnlyFn( - "aibtcdev-resources-v1", - "get-payment-address", - [], - address1 - ); - // parse into an object we can read - const currentPaymentAddress = cvToValue( - currentPaymentAddressResponse.result - ); - // set payment address - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "set-payment-address", - [ - Cl.standardPrincipal(currentPaymentAddress.value), - Cl.standardPrincipal(address1), - ], - deployer - ); - // ASSERT - expect(response.result).toBeOk(Cl.bool(true)); - }); - - it("set-payment-address() succeeds if called by current payment address", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const deployer = accounts.get("deployer")!; - const address1 = accounts.get("wallet_1")!; - const address2 = accounts.get("wallet_2")!; - - // ACT - // get current payment address - const currentPaymentAddressResponse = simnet.callReadOnlyFn( - "aibtcdev-resources-v1", - "get-payment-address", - [], - address1 - ); - // parse into an object we can read - const currentPaymentAddress = cvToValue( - currentPaymentAddressResponse.result - ); - // set payment address - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "set-payment-address", - [ - Cl.standardPrincipal(currentPaymentAddress.value), - Cl.standardPrincipal(address1), - ], - deployer - ); - // progress the chain - simnet.mineEmptyBlocks(5000); - // get current payment address again - const updatedPaymentAddressResponse = simnet.callReadOnlyFn( - "aibtcdev-resources-v1", - "get-payment-address", - [], - address1 - ); - // parse into an object we can read - const updatedPaymentAddress = cvToValue( - updatedPaymentAddressResponse.result - ); - // set payment address again - const secondResponse = simnet.callPublicFn( - "aibtcdev-resources-v1", - "set-payment-address", - [Cl.standardPrincipal(address1), Cl.standardPrincipal(address2)], - address1 - ); - // ASSERT - expect(updatedPaymentAddress.value).toEqual(address1); - expect(response.result).toBeOk(Cl.bool(true)); - expect(secondResponse.result).toBeOk(Cl.bool(true)); - }); -}); - -describe("Paying an Invoice", () => { - it("pay-invoice() fails if resource is not found", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const address1 = accounts.get("wallet_1")!; - // ACT - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "pay-invoice", - [ - Cl.uint(0), // resource index - Cl.none(), // memo - ], - address1 - ); - // ASSERT - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_RESOURCE_NOT_FOUND)); - }); - - it("pay-invoice() fails if resource is not enabled", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const deployer = accounts.get("deployer")!; - const address1 = accounts.get("wallet_1")!; - // ACT - // add a resource - simnet.callPublicFn( - "aibtcdev-resources-v1", - "add-resource", - testResource, - deployer - ); - // toggle resource - simnet.callPublicFn( - "aibtcdev-resources-v1", - "toggle-resource", - [Cl.uint(1)], - deployer - ); - // pay invoice for resource - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "pay-invoice", - [ - Cl.uint(1), // resource index - Cl.none(), // memo - ], - address1 - ); - // ASSERT - expect(response.result).toBeErr(Cl.uint(ErrCode.ERR_RESOURCE_NOT_ENABLED)); - }); - // not expecting ERR_USER_NOT_FOUND, not sure if we can force? - // it("pay-invoice() fails if user cannot be created or found", () => {}) - // not expecting ERR_INVOICE_HASH_NOT_FOUND, same as above - // it("pay-invoice() fails if invoice hash cannot be found", () => {}) - // not expecting ERR_SAVING_INVOICE in two spots, same as above - // it("pay-invoice() fails if invoice cannot be saved", () => {}) - it("pay-invoice() succeeds and returns invoice count without memo", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const deployer = accounts.get("deployer")!; - const address1 = accounts.get("wallet_1")!; - const expectedCount = 1; - // ACT - // mint aiBTC to pay for resources - simnet.callPublicFn( - "aibtcdev-aibtc", - "faucet-flood", - [Cl.principal(address1)], - address1 - ); - // add a resource - simnet.callPublicFn( - "aibtcdev-resources-v1", - "add-resource", - testResource, - deployer - ); - // pay invoice for resource - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "pay-invoice", - [ - Cl.uint(1), // resource index - Cl.none(), // memo - ], - address1 - ); - // ASSERT - expect(response.result).toBeOk(Cl.uint(expectedCount)); - }); - - it("pay-invoice() succeeds and returns invoice count with memo", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const deployer = accounts.get("deployer")!; - const address1 = accounts.get("wallet_1")!; - const expectedCount = 1; - // const memo = Buffer.from("This is a memo test!"); - const memo = new TextEncoder().encode("This is a memo test!"); - - // ACT - // mint aiBTC to pay for resources - simnet.callPublicFn( - "aibtcdev-aibtc", - "faucet-flood", - [Cl.principal(address1)], - address1 - ); - // add a resource - simnet.callPublicFn( - "aibtcdev-resources-v1", - "add-resource", - testResource, - deployer - ); - // pay invoice for resource - const response = simnet.callPublicFn( - "aibtcdev-resources-v1", - "pay-invoice", - [ - Cl.uint(1), // resource index - Cl.some(Cl.buffer(memo)), // memo - ], - address1 - ); - // ASSERT - expect(response.result).toBeOk(Cl.uint(expectedCount)); - }); - - it("pay-invoice() succeeds and returns invoice count with memo over several blocks", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const deployer = accounts.get("deployer")!; - const address1 = accounts.get("wallet_1")!; - const address2 = accounts.get("wallet_2")!; - const address3 = accounts.get("wallet_3")!; - const expectedCount = 1; - const memo = Cl.none(); - // ACT - // mint aiBTC to pay for resources - simnet.callPublicFn( - "aibtcdev-aibtc", - "faucet-flood", - [Cl.principal(address1)], - address1 - ); - // mint aiBTC to pay for resources - simnet.callPublicFn( - "aibtcdev-aibtc", - "faucet-flood", - [Cl.principal(address2)], - address2 - ); - // mint aiBTC to pay for resources - simnet.callPublicFn( - "aibtcdev-aibtc", - "faucet-flood", - [Cl.principal(address3)], - address3 - ); - // add a resource - simnet.callPublicFn( - "aibtcdev-resources-v1", - "add-resource", - testResource, - deployer - ); - // pay invoice once for 3 users - const blockResponses = [ - simnet.callPublicFn( - "aibtcdev-resources-v1", - "pay-invoice", - [ - Cl.uint(1), // resource index - memo, // memo - ], - address1 - ), - simnet.callPublicFn( - "aibtcdev-resources-v1", - "pay-invoice", - [ - Cl.uint(1), // resource index - memo, // memo - ], - address2 - ), - simnet.callPublicFn( - "aibtcdev-resources-v1", - "pay-invoice", - [ - Cl.uint(1), // resource index - memo, // memo - ], - address3 - ), - ]; - // progress by 5000 blocks - simnet.mineEmptyBlocks(5000); - // pay invoice again for 3 users - blockResponses.push( - simnet.callPublicFn( - "aibtcdev-resources-v1", - "pay-invoice", - [ - Cl.uint(1), // resource index - memo, // memo - ], - address1 - ), - simnet.callPublicFn( - "aibtcdev-resources-v1", - "pay-invoice", - [ - Cl.uint(1), // resource index - memo, // memo - ], - address2 - ), - simnet.callPublicFn( - "aibtcdev-resources-v1", - "pay-invoice", - [ - Cl.uint(1), // resource index - memo, // memo - ], - address3 - ) - ); - // ASSERT - for (let i = 0; i < blockResponses.length; i++) { - expect(blockResponses[i].result).toBeOk(Cl.uint(expectedCount + i)); - } - }); - - it("pay-invoice() succeeds and updates resource and user data", () => { - // ARRANGE - const accounts = simnet.getAccounts(); - const address1 = accounts.get("wallet_1")!; - const address2 = accounts.get("wallet_2")!; - const deployer = accounts.get("deployer")!; - const memo = Cl.none(); - // ACT - // mint aiBTC to pay for resources - simnet.callPublicFn( - "aibtcdev-aibtc", - "faucet-flood", - [Cl.principal(address1)], - address1 - ); - simnet.callPublicFn( - "aibtcdev-aibtc", - "faucet-flood", - [Cl.principal(address2)], - address2 - ); - // add a resource - simnet.callPublicFn( - "aibtcdev-resources-v1", - "add-resource", - testResource, - deployer - ); - const resourceBlock = simnet.blockHeight; - // pay invoice for resource - simnet.callPublicFn( - "aibtcdev-resources-v1", - "pay-invoice", - [ - Cl.uint(1), // resource index - memo, // memo - ], - address1 - ); - // progress the chain - simnet.mineEmptyBlocks(5000); - // pay invoice again for resource - simnet.callPublicFn( - "aibtcdev-resources-v1", - "pay-invoice", - [ - Cl.uint(1), // resource index - memo, // memo - ], - address2 - ); - // progress the chain - simnet.mineEmptyBlocks(5000); - // get resource - const resourceResponse = simnet.callReadOnlyFn( - "aibtcdev-resources-v1", - "get-resource", - [Cl.uint(1)], - deployer - ); - // get user - const userResponseOne = simnet.callReadOnlyFn( - "aibtcdev-resources-v1", - "get-user-data-by-address", - [Cl.standardPrincipal(address1)], - deployer - ); - const userResponseTwo = simnet.callReadOnlyFn( - "aibtcdev-resources-v1", - "get-user-data-by-address", - [Cl.standardPrincipal(address2)], - deployer - ); - // ASSERT - expect(resourceResponse.result).toBeSome( - Cl.tuple({ - createdAt: Cl.uint(resourceBlock), - enabled: Cl.bool(true), - description: Cl.stringUtf8("Generate a unique Bitcoin face."), - name: Cl.stringUtf8("Bitcoin Face"), - price: Cl.uint(defaultPrice), - totalSpent: Cl.uint(defaultPrice * 2), - totalUsed: Cl.uint(2), - }) - ); - expect(userResponseOne.result).toBeSome( - Cl.tuple({ - address: Cl.standardPrincipal(address1), - totalSpent: Cl.uint(defaultPrice), - totalUsed: Cl.uint(1), - }) - ); - expect(userResponseTwo.result).toBeSome( - Cl.tuple({ - address: Cl.standardPrincipal(address2), - totalSpent: Cl.uint(defaultPrice), - totalUsed: Cl.uint(1), - }) - ); - }); -}); diff --git a/tests/dao/aibtcdev-dao.test.ts b/tests/dao/aibtcdev-dao.test.ts new file mode 100644 index 0000000..835019d --- /dev/null +++ b/tests/dao/aibtcdev-dao.test.ts @@ -0,0 +1,59 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; + +const accounts = simnet.getAccounts(); +const address1 = accounts.get("wallet_1")!; +const address2 = accounts.get("wallet_2")!; +const addressDeployer = accounts.get("deployer")!; + +const contractAddress = `${addressDeployer}.aibtcdev-base-dao`; + +enum ErrCode { + ERR_UNAUTHORIZED = 1000, + ERR_NOT_DAO_OR_EXTENSION = 1001, + ERR_ALREADY_EXECUTED = 1002, + ERR_INVALID_EXTENSION = 1003, +} + +describe("aibtcdev-base-dao", () => { + // Extension Management Tests + describe("set-extension()", () => { + it("fails if caller is not DAO or extension"); + it("succeeds and sets extension status"); + }); + + describe("set-extensions()", () => { + it("fails if caller is not DAO or extension"); + it("succeeds and sets multiple extension statuses"); + }); + + // Execution Tests + describe("execute()", () => { + it("fails if caller is not DAO or extension"); + it("succeeds and executes proposal"); + }); + + // Construction Tests + describe("construct()", () => { + it("fails when called by an account that is not the deployer"); + it("fails when initializing the DAO with bootstrap proposal a second time"); + it("succeeds when initializing the DAO with bootstrap proposal"); + }); + + // Extension Callback Tests + describe("request-extension-callback()", () => { + it("fails if caller is not an extension"); + it("succeeds and calls an extension"); + }); + + // Query Tests + describe("is-extension()", () => { + it("succeeds and returns false with unrecognized extension"); + it("succeeds and returns true for active extensions"); + }); + + describe("executed-at()", () => { + it("succeeds and returns none with unrecognized proposal"); + it("succeeds and returns the Bitcoin block height the proposal was executed"); + }); +}); diff --git a/tests/dao/extensions/aibtc-ext001-actions.test.ts b/tests/dao/extensions/aibtc-ext001-actions.test.ts new file mode 100644 index 0000000..a60d9dc --- /dev/null +++ b/tests/dao/extensions/aibtc-ext001-actions.test.ts @@ -0,0 +1,1079 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; + +const accounts = simnet.getAccounts(); +const address1 = accounts.get("wallet_1")!; +const address2 = accounts.get("wallet_2")!; +const addressDeployer = accounts.get("deployer")!; + +const contractAddress = `${addressDeployer}.aibtc-ext001-actions`; + +enum ErrCode { + ERR_UNAUTHORIZED = 1000, + ERR_NOT_DAO_OR_EXTENSION, + + ERR_NOT_INITIALIZED = 1100, + ERR_ALREADY_INITIALIZED, + + ERR_TREASURY_MUST_BE_CONTRACT = 1200, + ERR_TREASURY_CANNOT_BE_SELF, + ERR_TREASURY_ALREADY_SET, + ERR_TREASURY_MISMATCH, + + ERR_TOKEN_MUST_BE_CONTRACT = 1300, + ERR_TOKEN_NOT_INITIALIZED, + ERR_TOKEN_MISMATCH, + ERR_INSUFFICIENT_BALANCE, + + ERR_PROPOSAL_NOT_FOUND = 1400, + ERR_PROPOSAL_ALREADY_EXECUTED, + ERR_PROPOSAL_STILL_ACTIVE, + ERR_SAVING_PROPOSAL, + ERR_PROPOSAL_ALREADY_CONCLUDED, + + ERR_VOTE_TOO_SOON = 1500, + ERR_VOTE_TOO_LATE, + ERR_ALREADY_VOTED, + ERR_ZERO_VOTING_POWER, + ERR_QUORUM_NOT_REACHED, + + ERR_INVALID_ACTION = 1600, + ERR_INVALID_PARAMETERS, +} + +describe("aibtc-ext001-actions", () => { + // Protocol Treasury Tests + describe("set-protocol-treasury()", () => { + it("fails if caller is not DAO or extension", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "set-protocol-treasury", + [Cl.contractPrincipal(addressDeployer, "test-treasury")], + address1 + ); + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION)); + }); + + it("fails if treasury is not a contract", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "set-protocol-treasury", + [Cl.standardPrincipal(address1)], + addressDeployer + ); + expect(receipt.result).toBeErr( + Cl.uint(ErrCode.ERR_TREASURY_MUST_BE_CONTRACT) + ); + }); + + it("fails if treasury is self", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "set-protocol-treasury", + [Cl.contractPrincipal(addressDeployer, "aibtc-ext001-actions")], + addressDeployer + ); + expect(receipt.result).toBeErr( + Cl.uint(ErrCode.ERR_TREASURY_CANNOT_BE_SELF) + ); + }); + + it("fails if treasury is already set", () => { + // First set the treasury + simnet.callPublicFn( + contractAddress, + "set-protocol-treasury", + [Cl.contractPrincipal(addressDeployer, "test-treasury")], + addressDeployer + ); + + // Try to set it to the same value + const receipt = simnet.callPublicFn( + contractAddress, + "set-protocol-treasury", + [Cl.contractPrincipal(addressDeployer, "test-treasury")], + addressDeployer + ); + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_TREASURY_ALREADY_SET)); + }); + + it("succeeds and sets new treasury", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "set-protocol-treasury", + [Cl.contractPrincipal(addressDeployer, "test-treasury")], + addressDeployer + ); + expect(receipt.result).toBeOk(Cl.bool(true)); + + // Verify treasury was set + const getReceipt = simnet.callReadOnlyFn( + contractAddress, + "get-protocol-treasury", + [], + addressDeployer + ); + expect(getReceipt.result).toBeOk( + Cl.some(Cl.contractPrincipal(addressDeployer, "test-treasury")) + ); + }); + }); + + // Voting Token Tests + describe("set-voting-token()", () => { + it("fails if caller is not DAO or extension", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "set-voting-token", + [Cl.contractPrincipal(addressDeployer, "test-token")], + address1 + ); + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION)); + }); + + it("fails if token is not a contract", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "set-voting-token", + [Cl.standardPrincipal(address1)], + addressDeployer + ); + expect(receipt.result).toBeErr( + Cl.uint(ErrCode.ERR_TOKEN_MUST_BE_CONTRACT) + ); + }); + + it("fails if token is not initialized", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "set-voting-token", + [Cl.contractPrincipal(addressDeployer, "test-token")], + addressDeployer + ); + expect(receipt.result).toBeErr( + Cl.uint(ErrCode.ERR_TOKEN_NOT_INITIALIZED) + ); + }); + + it("fails if token mismatches", () => { + // First initialize the token + simnet.callPublicFn( + contractAddress, + "set-voting-token", + [Cl.contractPrincipal(addressDeployer, "test-token")], + addressDeployer + ); + + // Try to set a different token + const receipt = simnet.callPublicFn( + contractAddress, + "set-voting-token", + [Cl.contractPrincipal(addressDeployer, "different-token")], + addressDeployer + ); + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_TOKEN_MISMATCH)); + }); + + it("succeeds and sets new token", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "set-voting-token", + [Cl.contractPrincipal(addressDeployer, "test-token")], + addressDeployer + ); + expect(receipt.result).toBeOk(Cl.bool(true)); + + // Verify token was set + const getReceipt = simnet.callReadOnlyFn( + contractAddress, + "get-voting-token", + [], + addressDeployer + ); + expect(getReceipt.result).toBeOk( + Cl.some(Cl.contractPrincipal(addressDeployer, "test-token")) + ); + }); + }); + + // Proposal Tests + describe("propose-action()", () => { + it("fails if contract not initialized", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "propose-action", + [ + Cl.stringAscii("send-message"), + Cl.list([Cl.stringUtf8("Hello World")]), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_NOT_INITIALIZED)); + }); + + it("fails if token mismatches", () => { + // First set the treasury and token + simnet.callPublicFn( + contractAddress, + "set-protocol-treasury", + [Cl.contractPrincipal(addressDeployer, "test-treasury")], + addressDeployer + ); + + simnet.callPublicFn( + contractAddress, + "set-voting-token", + [Cl.contractPrincipal(addressDeployer, "test-token")], + addressDeployer + ); + + const receipt = simnet.callPublicFn( + contractAddress, + "propose-action", + [ + Cl.stringAscii("send-message"), + Cl.list([Cl.stringUtf8("Hello World")]), + Cl.contractPrincipal(addressDeployer, "wrong-token"), + ], + address1 + ); + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_TOKEN_MISMATCH)); + }); + + it("fails if caller has no balance", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "propose-action", + [ + Cl.stringAscii("send-message"), + Cl.list([Cl.stringUtf8("Hello World")]), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_INSUFFICIENT_BALANCE)); + }); + + it("fails if action is invalid", () => { + // Mock some balance for the caller + + simnet.callPublicFn( + `${addressDeployer}.test-token`, + "mint", + [Cl.uint(1000000), Cl.standardPrincipal(address1)], + addressDeployer + ); + + const receipt = simnet.callPublicFn( + contractAddress, + "propose-action", + [ + Cl.stringAscii("invalid-action"), + Cl.list([Cl.stringUtf8("Hello World")]), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_INVALID_ACTION)); + }); + + it("fails if parameters are invalid", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "propose-action", + [ + Cl.stringAscii("send-message"), + Cl.list([]), // Empty parameters + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_INVALID_PARAMETERS)); + }); + + /* TODO: fix test below + it("succeeds and creates new proposal", () => { + // Mock some balance for the caller + simnet.callPublicFn( + `${addressDeployer}.test-token`, + "mint", + [Cl.uint(1000000), Cl.standardPrincipal(address1)], + addressDeployer + ); + + const receipt = simnet.callPublicFn( + contractAddress, + "propose-action", + [ + Cl.stringAscii("send-message"), + Cl.list([Cl.stringUtf8("Hello World")]), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + expect(receipt.result).toBeOk(Cl.uint(1)); + + // Verify proposal was created + const getReceipt = simnet.callReadOnlyFn( + contractAddress, + "get-total-proposals", + [], + addressDeployer + ); + expect(getReceipt.result).toBeOk(Cl.uint(1)); + + // Verify proposal details + const proposalReceipt = simnet.callReadOnlyFn( + contractAddress, + "get-proposal", + [Cl.uint(1)], + addressDeployer + ); + const proposal = proposalReceipt.result.expectSome().expectTuple(); + expect(proposal.action).toBe("send-message"); + expect(proposal.concluded).toBe(false); + expect(proposal.passed).toBe(false); + expect(proposal.votesFor).toBe(0); + expect(proposal.votesAgainst).toBe(0); + }); + */ + }); + + // Voting Tests + describe("vote-on-proposal()", () => { + it("fails if contract not initialized", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "vote-on-proposal", + [ + Cl.uint(1), + Cl.contractPrincipal(addressDeployer, "test-token"), + Cl.bool(true), + ], + address1 + ); + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_NOT_INITIALIZED)); + }); + + it("fails if token mismatches", () => { + // First set the treasury and token + simnet.callPublicFn( + contractAddress, + "set-protocol-treasury", + [Cl.contractPrincipal(addressDeployer, "test-treasury")], + addressDeployer + ); + + simnet.callPublicFn( + contractAddress, + "set-voting-token", + [Cl.contractPrincipal(addressDeployer, "test-token")], + addressDeployer + ); + + const receipt = simnet.callPublicFn( + contractAddress, + "vote-on-proposal", + [ + Cl.uint(1), + Cl.contractPrincipal(addressDeployer, "wrong-token"), + Cl.bool(true), + ], + address1 + ); + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_TOKEN_MISMATCH)); + }); + + it("fails if caller has no balance", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "vote-on-proposal", + [ + Cl.uint(1), + Cl.contractPrincipal(addressDeployer, "test-token"), + Cl.bool(true), + ], + address1 + ); + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_INSUFFICIENT_BALANCE)); + }); + + it("fails if voting too soon", () => { + // Mock some balance for the caller + simnet.callPublicFn( + `${addressDeployer}.test-token`, + "mint", + [Cl.uint(1000000), Cl.standardPrincipal(address1)], + addressDeployer + ); + + // Create a proposal + simnet.callPublicFn( + contractAddress, + "propose-action", + [ + Cl.stringAscii("send-message"), + Cl.list([Cl.stringUtf8("Hello World")]), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + + // Try to vote before start block + const receipt = simnet.callPublicFn( + contractAddress, + "vote-on-proposal", + [ + Cl.uint(1), + Cl.contractPrincipal(addressDeployer, "test-token"), + Cl.bool(true), + ], + address1 + ); + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_VOTE_TOO_SOON)); + }); + + it("fails if voting too late", () => { + // Mine blocks past the voting period + simnet.mineEmptyBlocks(145); + + const receipt = simnet.callPublicFn( + contractAddress, + "vote-on-proposal", + [ + Cl.uint(1), + Cl.contractPrincipal(addressDeployer, "test-token"), + Cl.bool(true), + ], + address1 + ); + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_VOTE_TOO_LATE)); + }); + + it("fails if proposal concluded", () => { + // Conclude the proposal + simnet.callPublicFn( + contractAddress, + "conclude-proposal", + [ + Cl.uint(1), + Cl.contractPrincipal(addressDeployer, "test-treasury"), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + + const receipt = simnet.callPublicFn( + contractAddress, + "vote-on-proposal", + [ + Cl.uint(1), + Cl.contractPrincipal(addressDeployer, "test-token"), + Cl.bool(true), + ], + address1 + ); + expect(receipt.result).toBeErr( + Cl.uint(ErrCode.ERR_PROPOSAL_ALREADY_CONCLUDED) + ); + }); + + it("fails if already voted", () => { + // Create a new proposal + simnet.callPublicFn( + contractAddress, + "propose-action", + [ + Cl.stringAscii("send-message"), + Cl.list([Cl.stringUtf8("Hello World")]), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + + // Vote once + simnet.callPublicFn( + contractAddress, + "vote-on-proposal", + [ + Cl.uint(2), + Cl.contractPrincipal(addressDeployer, "test-token"), + Cl.bool(true), + ], + address1 + ); + + // Try to vote again + const receipt = simnet.callPublicFn( + contractAddress, + "vote-on-proposal", + [ + Cl.uint(2), + Cl.contractPrincipal(addressDeployer, "test-token"), + Cl.bool(true), + ], + address1 + ); + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_ALREADY_VOTED)); + }); + + it("succeeds and records vote", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "vote-on-proposal", + [ + Cl.uint(2), + Cl.contractPrincipal(addressDeployer, "test-token"), + Cl.bool(true), + ], + address2 + ); + expect(receipt.result).toBeOk(Cl.bool(true)); + + // Verify vote was recorded + const getReceipt = simnet.callReadOnlyFn( + contractAddress, + "get-total-votes", + [Cl.uint(2), Cl.standardPrincipal(address2)], + addressDeployer + ); + expect(getReceipt.result).toBeOk(Cl.uint(1000000)); + }); + }); + + // Conclusion Tests + describe("conclude-proposal()", () => { + it("fails if contract not initialized", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "conclude-proposal", + [ + Cl.uint(1), + Cl.contractPrincipal(addressDeployer, "test-treasury"), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_NOT_INITIALIZED)); + }); + + it("fails if treasury mismatches", () => { + // First set the treasury and token + simnet.callPublicFn( + contractAddress, + "set-protocol-treasury", + [Cl.contractPrincipal(addressDeployer, "test-treasury")], + addressDeployer + ); + + simnet.callPublicFn( + contractAddress, + "set-voting-token", + [Cl.contractPrincipal(addressDeployer, "test-token")], + addressDeployer + ); + + const receipt = simnet.callPublicFn( + contractAddress, + "conclude-proposal", + [ + Cl.uint(1), + Cl.contractPrincipal(addressDeployer, "wrong-treasury"), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_TREASURY_MISMATCH)); + }); + + it("fails if proposal still active", () => { + // Create a new proposal + simnet.callPublicFn( + contractAddress, + "propose-action", + [ + Cl.stringAscii("send-message"), + Cl.list([Cl.stringUtf8("Hello World")]), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + + const receipt = simnet.callPublicFn( + contractAddress, + "conclude-proposal", + [ + Cl.uint(3), + Cl.contractPrincipal(addressDeployer, "test-treasury"), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + expect(receipt.result).toBeErr( + Cl.uint(ErrCode.ERR_PROPOSAL_STILL_ACTIVE) + ); + }); + + it("fails if proposal already concluded", () => { + // Mine blocks to end voting period + simnet.mineEmptyBlocks(144); + + // Conclude the proposal first time + simnet.callPublicFn( + contractAddress, + "conclude-proposal", + [ + Cl.uint(3), + Cl.contractPrincipal(addressDeployer, "test-treasury"), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + + // Try to conclude again + const receipt = simnet.callPublicFn( + contractAddress, + "conclude-proposal", + [ + Cl.uint(3), + Cl.contractPrincipal(addressDeployer, "test-treasury"), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + expect(receipt.result).toBeErr( + Cl.uint(ErrCode.ERR_PROPOSAL_ALREADY_CONCLUDED) + ); + }); + + it("succeeds and executes if passed", () => { + // Create a new proposal + simnet.callPublicFn( + contractAddress, + "propose-action", + [ + Cl.stringAscii("send-message"), + Cl.list([Cl.stringUtf8("Hello World")]), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + + // Vote in favor with enough tokens to pass + simnet.callPublicFn( + contractAddress, + "vote-on-proposal", + [ + Cl.uint(4), + Cl.contractPrincipal(addressDeployer, "test-token"), + Cl.bool(true), + ], + address1 + ); + + // Mine blocks to end voting period + simnet.mineEmptyBlocks(144); + + const receipt = simnet.callPublicFn( + contractAddress, + "conclude-proposal", + [ + Cl.uint(4), + Cl.contractPrincipal(addressDeployer, "test-treasury"), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + expect(receipt.result).toBeOk(Cl.bool(true)); + }); + + it("succeeds without executing if failed", () => { + // Create a new proposal + simnet.callPublicFn( + contractAddress, + "propose-action", + [ + Cl.stringAscii("send-message"), + Cl.list([Cl.stringUtf8("Hello World")]), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + + // Vote against with enough tokens to fail + simnet.callPublicFn( + contractAddress, + "vote-on-proposal", + [ + Cl.uint(5), + Cl.contractPrincipal(addressDeployer, "test-token"), + Cl.bool(false), + ], + address1 + ); + + // Mine blocks to end voting period + simnet.mineEmptyBlocks(144); + + const receipt = simnet.callPublicFn( + contractAddress, + "conclude-proposal", + [ + Cl.uint(5), + Cl.contractPrincipal(addressDeployer, "test-treasury"), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + expect(receipt.result).toBe(Cl.bool(false)); + }); + }); + + // Getter Tests + describe("get-voting-period()", () => { + it("returns the correct voting period", () => { + const receipt = simnet.callReadOnlyFn( + contractAddress, + "get-voting-period", + [], + addressDeployer + ); + expect(receipt.result).toBe(Cl.uint(144)); // 144 blocks, ~1 day + }); + }); + + describe("get-voting-quorum()", () => { + it("returns the correct voting quorum", () => { + const receipt = simnet.callReadOnlyFn( + contractAddress, + "get-voting-quorum", + [], + addressDeployer + ); + expect(receipt.result).toBe(Cl.uint(66)); // 66% of liquid supply + }); + }); + + describe("is-initialized()", () => { + it("returns false when treasury and token not set", () => { + // Reset contract state + simnet.mineBlock([]); + + const receipt = simnet.callReadOnlyFn( + contractAddress, + "is-initialized", + [], + addressDeployer + ); + expect(receipt.result).toBeBool(false); + }); + + it("returns true when treasury and token are set", () => { + // Set treasury and token + simnet.callPublicFn( + contractAddress, + "set-protocol-treasury", + [Cl.contractPrincipal(addressDeployer, "test-treasury")], + addressDeployer + ); + simnet.callPublicFn( + contractAddress, + "set-voting-token", + [Cl.contractPrincipal(addressDeployer, "test-token")], + addressDeployer + ); + + const receipt = simnet.callReadOnlyFn( + contractAddress, + "is-initialized", + [], + addressDeployer + ); + expect(receipt.result).toBeBool(true); + }); + }); + + describe("get-protocol-treasury()", () => { + it("returns none when treasury not set", () => { + // Reset contract state + simnet.mineBlock([]); + + const receipt = simnet.callReadOnlyFn( + contractAddress, + "get-protocol-treasury", + [], + addressDeployer + ); + expect(receipt.result).toBe(Cl.none()); + }); + + it("returns some with treasury address when set", () => { + // Set treasury + simnet.callPublicFn( + contractAddress, + "set-protocol-treasury", + [Cl.contractPrincipal(addressDeployer, "test-treasury")], + addressDeployer + ); + + const receipt = simnet.callReadOnlyFn( + contractAddress, + "get-protocol-treasury", + [], + addressDeployer + ); + expect(receipt.result).toBeOk( + Cl.some(Cl.contractPrincipal(addressDeployer, "test-treasury")) + ); + }); + }); + + describe("get-voting-token()", () => { + it("returns none when token not set", () => { + // Reset contract state + simnet.mineBlock([]); + + const receipt = simnet.callReadOnlyFn( + contractAddress, + "get-voting-token", + [], + addressDeployer + ); + expect(receipt.result).toBe(Cl.none()); + }); + + it("returns some with token address when set", () => { + // Set token + simnet.callPublicFn( + contractAddress, + "set-voting-token", + [Cl.contractPrincipal(addressDeployer, "test-token")], + addressDeployer + ); + + const receipt = simnet.callReadOnlyFn( + contractAddress, + "get-voting-token", + [], + addressDeployer + ); + expect(receipt.result).toBeOk( + Cl.some(Cl.contractPrincipal(addressDeployer, "test-token")) + ); + }); + }); + + describe("get-proposal()", () => { + it("returns none for non-existent proposal", () => { + const receipt = simnet.callReadOnlyFn( + contractAddress, + "get-proposal", + [Cl.uint(999)], + addressDeployer + ); + expect(receipt.result).toBeOk(Cl.none()); + }); + + /* TODO: fix test below + it("returns proposal details for existing proposal", () => { + // Create a proposal first + simnet.callPublicFn( + contractAddress, + "propose-action", + [ + Cl.stringAscii("send-message"), + Cl.list([Cl.stringUtf8("Hello World")]), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ), + + const receipt = simnet.callReadOnlyFn( + contractAddress, + "get-proposal", + [Cl.uint(1)], + addressDeployer + ); + + const proposal = receipt.result.expectOk().expectSome().expectTuple(); + expect(proposal.action).toBe("send-message"); + expect(proposal.concluded).toBe(false); + expect(proposal.passed).toBe(false); + expect(proposal.votesFor).toBe(0); + expect(proposal.votesAgainst).toBe(0); + }); + */ + }); + + describe("get-total-proposals()", () => { + it("returns 0 when no proposals exist", () => { + // Reset contract state + simnet.mineBlock([]); + + const receipt = simnet.callReadOnlyFn( + contractAddress, + "get-total-proposals", + [], + addressDeployer + ); + expect(receipt.result).toBe(Cl.uint(0)); + }); + + it("returns correct count after creating proposals", () => { + // Create two proposals + simnet.callPublicFn( + contractAddress, + "propose-action", + [ + Cl.stringAscii("send-message"), + Cl.list([Cl.stringUtf8("First")]), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + simnet.callPublicFn( + contractAddress, + "propose-action", + [ + Cl.stringAscii("send-message"), + Cl.list([Cl.stringUtf8("Second")]), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + + const receipt = simnet.callReadOnlyFn( + contractAddress, + "get-total-proposals", + [], + addressDeployer + ); + expect(receipt.result).toBeOk(Cl.uint(2)); + }); + }); + + describe("get-total-votes()", () => { + it("returns 0 for proposal with no votes", () => { + const receipt = simnet.callReadOnlyFn( + contractAddress, + "get-total-votes", + [Cl.uint(1), Cl.standardPrincipal(address1)], + addressDeployer + ); + expect(receipt.result).toBeOk(Cl.uint(0)); + }); + + it("returns correct vote amount for proposal with votes", () => { + // Create proposal and vote + simnet.callPublicFn( + contractAddress, + "propose-action", + [ + Cl.stringAscii("send-message"), + Cl.list([Cl.stringUtf8("Hello World")]), + Cl.contractPrincipal(addressDeployer, "test-token"), + ], + address1 + ); + simnet.callPublicFn( + contractAddress, + "vote-on-proposal", + [ + Cl.uint(1), + Cl.contractPrincipal(addressDeployer, "test-token"), + Cl.bool(true), + ], + address1 + ); + + const receipt = simnet.callReadOnlyFn( + contractAddress, + "get-total-votes", + [Cl.uint(1), Cl.standardPrincipal(address1)], + addressDeployer + ); + expect(receipt.result).toBeOk(Cl.uint(1000000)); // Amount from previous mint + }); + }); + + describe("get-voting-period()", () => { + it("returns the correct voting period", () => { + const receipt = simnet.callReadOnlyFn( + contractAddress, + "get-voting-period", + [], + addressDeployer + ); + expect(receipt.result).toBeOk(Cl.uint(144)); // 144 blocks, ~1 day + }); + }); + + describe("get-voting-quorum()", () => { + it("returns the correct voting quorum", () => { + const receipt = simnet.callReadOnlyFn( + contractAddress, + "get-voting-quorum", + [], + addressDeployer + ); + expect(receipt.result).toBeOk(Cl.uint(66)); // 66% of liquid supply + }); + }); + + describe("is-initialized()", () => { + it("returns false when treasury and token not set", () => { + // Reset contract state + simnet.mineBlock([]); + + const receipt = simnet.callReadOnlyFn( + contractAddress, + "is-initialized", + [], + addressDeployer + ); + expect(receipt.result).toBeOk(Cl.bool(false)); + }); + + /* TODO: fix test below + it("returns true when treasury and token are set", () => { + // Set treasury and token + simnet.callPublicFn( + contractAddress, + "set-protocol-treasury", + [Cl.contractPrincipal(addressDeployer, "test-treasury")], + addressDeployer + ) + simnet.callPublicFn( + contractAddress, + "set-voting-token", + [Cl.contractPrincipal(addressDeployer, "test-token")], + addressDeployer + ), + + const receipt = simnet.callReadOnlyFn( + contractAddress, + "is-initialized", + [], + addressDeployer + ).result; + expect(receipt).toBeOk(Cl.bool(true)); + }); + */ + }); + + describe("callback()", () => { + it("succeeds with any sender and memo", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "callback", + [ + Cl.standardPrincipal(address1), + Cl.buffer(new TextEncoder().encode("memo")), + ], + address1 + ); + expect(receipt.result).toBeOk(Cl.bool(true)); + }); + }); +}); diff --git a/tests/dao/extensions/aibtc-ext002-bank-account.test.ts b/tests/dao/extensions/aibtc-ext002-bank-account.test.ts new file mode 100644 index 0000000..e0e5738 --- /dev/null +++ b/tests/dao/extensions/aibtc-ext002-bank-account.test.ts @@ -0,0 +1,62 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; + +const accounts = simnet.getAccounts(); +const address1 = accounts.get("wallet_1")!; +const address2 = accounts.get("wallet_2")!; +const addressDeployer = accounts.get("deployer")!; + +const contractAddress = `${addressDeployer}.aibtc-ext002-bank-account`; + +enum ErrCode { + ERR_INVALID = 2000, + ERR_UNAUTHORIZED, + ERR_TOO_SOON, + ERR_INVALID_AMOUNT, +} + +const withdrawalAmount = 10000000; // 10 STX +const withdrawalPeriod = 144; // 144 blocks + +describe("aibtc-ext002-bank-account", () => { + // Account Holder Tests + describe("set-account-holder()", () => { + it("fails if caller is not DAO or extension"); + it("fails if old address matches current holder"); + it("succeeds and sets new account holder"); + }); + + // Withdrawal Period Tests + describe("set-withdrawal-period()", () => { + it("fails if caller is not DAO or extension"); + it("fails if period is 0"); + it("succeeds and sets new withdrawal period"); + }); + + // Withdrawal Amount Tests + describe("set-withdrawal-amount()", () => { + it("fails if caller is not DAO or extension"); + it("fails if amount is 0"); + it("succeeds and sets new withdrawal amount"); + }); + + // Last Withdrawal Block Tests + describe("override-last-withdrawal-block()", () => { + it("fails if caller is not DAO or extension"); + it("fails if block is before deployment"); + it("succeeds and sets new last withdrawal block"); + }); + + // Deposit Tests + describe("deposit-stx()", () => { + it("fails if amount is 0"); + it("succeeds and transfers STX to contract"); + }); + + // Withdrawal Tests + describe("withdraw-stx()", () => { + it("fails if caller is not account holder"); + it("fails if withdrawing too soon"); + it("succeeds and transfers STX to account holder"); + }); +}); diff --git a/tests/dao/extensions/aibtc-ext003-direct-execute.test.ts b/tests/dao/extensions/aibtc-ext003-direct-execute.test.ts new file mode 100644 index 0000000..fc383a5 --- /dev/null +++ b/tests/dao/extensions/aibtc-ext003-direct-execute.test.ts @@ -0,0 +1,92 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; + +const accounts = simnet.getAccounts(); +const address1 = accounts.get("wallet_1")!; +const address2 = accounts.get("wallet_2")!; +const addressDeployer = accounts.get("deployer")!; + +const contractAddress = `${addressDeployer}.aibtc-ext003-direct-execute`; + +enum ErrCode { + ERR_UNAUTHORIZED = 3000, + ERR_NOT_DAO_OR_EXTENSION, + + ERR_NOT_INITIALIZED = 3100, + ERR_ALREADY_INITIALIZED, + + ERR_TREASURY_MUST_BE_CONTRACT = 3200, + ERR_TREASURY_CANNOT_BE_SELF, + ERR_TREASURY_ALREADY_SET, + ERR_TREASURY_MISMATCH, + + ERR_TOKEN_MUST_BE_CONTRACT = 3300, + ERR_TOKEN_NOT_INITIALIZED, + ERR_TOKEN_MISMATCH, + ERR_INSUFFICIENT_BALANCE, + + ERR_PROPOSAL_NOT_FOUND = 3400, + ERR_PROPOSAL_ALREADY_EXECUTED, + ERR_PROPOSAL_STILL_ACTIVE, + ERR_SAVING_PROPOSAL, + ERR_PROPOSAL_ALREADY_CONCLUDED, + + ERR_VOTE_TOO_SOON = 3500, + ERR_VOTE_TOO_LATE, + ERR_ALREADY_VOTED, + ERR_ZERO_VOTING_POWER, + ERR_QUORUM_NOT_REACHED, +} + +describe("aibtc-ext003-direct-execute", () => { + // Protocol Treasury Tests + describe("set-protocol-treasury()", () => { + it("fails if caller is not DAO or extension"); + it("fails if treasury is not a contract"); + it("fails if treasury is self"); + it("fails if treasury is already set"); + it("succeeds and sets new treasury"); + }); + + // Voting Token Tests + describe("set-voting-token()", () => { + it("fails if caller is not DAO or extension"); + it("fails if token is not a contract"); + it("fails if token is not initialized"); + it("fails if token mismatches"); + it("succeeds and sets new token"); + }); + + // Proposal Tests + describe("create-proposal()", () => { + it("fails if contract not initialized"); + it("fails if token mismatches"); + it("fails if caller has no balance"); + it("fails if proposal already executed"); + it("succeeds and creates new proposal"); + }); + + // Voting Tests + describe("vote-on-proposal()", () => { + it("fails if contract not initialized"); + it("fails if token mismatches"); + it("fails if caller has no balance"); + it("fails if proposal already executed"); + it("fails if voting too soon"); + it("fails if voting too late"); + it("fails if proposal concluded"); + it("fails if already voted"); + it("succeeds and records vote"); + }); + + // Conclusion Tests + describe("conclude-proposal()", () => { + it("fails if contract not initialized"); + it("fails if treasury mismatches"); + it("fails if proposal already executed"); + it("fails if proposal still active"); + it("fails if proposal already concluded"); + it("succeeds and executes if passed"); + it("succeeds without executing if failed"); + }); +}); diff --git a/tests/dao/extensions/aibtc-ext004-messaging.test.ts b/tests/dao/extensions/aibtc-ext004-messaging.test.ts new file mode 100644 index 0000000..0eac483 --- /dev/null +++ b/tests/dao/extensions/aibtc-ext004-messaging.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; + +const accounts = simnet.getAccounts(); +const address1 = accounts.get("wallet_1")!; +const address2 = accounts.get("wallet_2")!; +const addressDeployer = accounts.get("deployer")!; + +const contractAddress = `${addressDeployer}.aibtc-ext004-messaging`; + +enum ErrCode { + ERR_UNAUTHORIZED = 4000, +} + +describe("aibtc-ext004-messaging", () => { + // Message Tests + describe("send()", () => { + it("succeeds if called by any user with isFromDao false"); + it("fails if called by any user with isFromDao true"); + it("succeeds if called by a DAO proposal with isFromDao true"); + }); +}); diff --git a/tests/dao/extensions/aibtc-ext005-payments.test.ts b/tests/dao/extensions/aibtc-ext005-payments.test.ts new file mode 100644 index 0000000..c5353a0 --- /dev/null +++ b/tests/dao/extensions/aibtc-ext005-payments.test.ts @@ -0,0 +1,75 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; + +const accounts = simnet.getAccounts(); +const address1 = accounts.get("wallet_1")!; +const address2 = accounts.get("wallet_2")!; +const addressDeployer = accounts.get("deployer")!; + +const contractAddress = `${addressDeployer}.aibtc-ext005-payments`; + +enum ErrCode { + ERR_UNAUTHORIZED = 5000, + ERR_INVALID_PARAMS, + ERR_NAME_ALREADY_USED, + ERR_SAVING_RESOURCE_DATA, + ERR_DELETING_RESOURCE_DATA, + ERR_RESOURCE_NOT_FOUND, + ERR_RESOURCE_DISABLED, + ERR_USER_ALREADY_EXISTS, + ERR_SAVING_USER_DATA, + ERR_USER_NOT_FOUND, + ERR_INVOICE_ALREADY_PAID, + ERR_SAVING_INVOICE_DATA, + ERR_INVOICE_NOT_FOUND, + ERR_RECENT_PAYMENT_NOT_FOUND, +} + +describe("aibtc-ext005-payments", () => { + // Payment Address Tests + describe("set-payment-address()", () => { + it("fails if caller is not DAO or extension"); + it("fails if old address matches current payment address"); + it("fails if old address and new address are the same"); + it("succeeds and sets the new payment address"); + }); + + // Resource Tests + describe("add-resource()", () => { + it("fails if caller is not DAO or extension"); + it("fails if name is blank"); + it("fails if description is blank"); + it("fails if price is 0"); + it("fails if provided url is blank"); + it("fails if resource name already used"); + it("succeeds and adds a new resource"); + }); + + // Resource Toggle Tests + describe("toggle-resource()", () => { + it("fails if caller is not DAO or extension"); + it("fails if resource is not found"); + it("fails if resource index is 0"); + it("succeeds and toggles if resource is enabled"); + }); + + describe("toggle-resource-by-name()", () => { + it("fails if caller is not DAO or extension"); + it("fails if resource is not found"); + it("succeeds and toggles if resource is enabled"); + }); + + // Invoice Tests + describe("pay-invoice()", () => { + it("fails if resource is not found"); + it("fails if resource index is 0"); + it("fails if resource is disabled"); + it("succeeds and updates info for resource"); + }); + + describe("pay-invoice-by-resource-name()", () => { + it("fails if resource is not found"); + it("fails if resource is disabled"); + it("succeeds and updates info for resource"); + }); +}); diff --git a/tests/dao/extensions/aibtc-ext006-treasury.test.ts b/tests/dao/extensions/aibtc-ext006-treasury.test.ts new file mode 100644 index 0000000..bdcd68b --- /dev/null +++ b/tests/dao/extensions/aibtc-ext006-treasury.test.ts @@ -0,0 +1,81 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; + +const accounts = simnet.getAccounts(); +const address1 = accounts.get("wallet_1")!; +const address2 = accounts.get("wallet_2")!; +const addressDeployer = accounts.get("deployer")!; + +const contractAddress = `${addressDeployer}.aibtc-ext006-treasury`; + +enum ErrCode { + ERR_UNAUTHORIZED = 6000, + ERR_UNKNOWN_ASSSET = 6001, +} + +describe("aibtc-ext006-treasury", () => { + // Allow Asset Tests + describe("allow-asset()", () => { + it("fails if caller is not DAO or extension"); + it("succeeds and sets new allowed asset"); + it("succeeds and toggles status of existing asset"); + }); + + // Allow Assets Tests + describe("allow-assets()", () => { + it("fails if caller is not DAO or extension"); + it("succeeds and sets new allowed assets"); + it("succeeds and toggles status of existing assets"); + }); + + // Deposit STX Tests + describe("deposit-stx()", () => { + it("succeeds and deposits STX to the treasury"); + }); + + // Deposit FT Tests + describe("deposit-ft()", () => { + it("fails if asset is not allowed"); + it("succeeds and transfers FT to treasury"); + }); + + // Deposit NFT Tests + describe("deposit-nft()", () => { + it("fails if asset is not allowed"); + it("succeeds and transfers NFT to treasury"); + }); + + // Withdraw STX Tests + describe("withdraw-stx()", () => { + it("fails if caller is not DAO or extension"); + it("succeeds and transfers STX to a standard principal"); + it("succeeds and transfers STX to a contract principal"); + }); + + // Withdraw FT Tests + describe("withdraw-ft()", () => { + it("fails if caller is not DAO or extension"); + it("succeeds and transfers FT to a standard principal"); + it("succeeds and transfers FT to a contract principal"); + }); + + // Withdraw NFT Tests + describe("withdraw-nft()", () => { + it("fails if caller is not DAO or extension"); + it("succeeds and transfers NFT to a standard principal"); + it("succeeds and transfers NFT to a contract principal"); + }); + + // Delegate STX Tests + describe("delegate-stx()", () => { + it("fails if caller is not DAO or extension"); + it("succeeds and delegates to Stacks PoX"); + }); + + // Revoke Delegate STX Tests + describe("revoke-delegate-stx()", () => { + it("fails if caller is not DAO or extension"); + it("fails if contract is not currently stacking"); + it("succeeds and revokes stacking delegation"); + }); +}); diff --git a/tests/dao/proposals/aibtc001-bootstrap.test.ts b/tests/dao/proposals/aibtc001-bootstrap.test.ts new file mode 100644 index 0000000..6869bb1 --- /dev/null +++ b/tests/dao/proposals/aibtc001-bootstrap.test.ts @@ -0,0 +1,16 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; + +const accounts = simnet.getAccounts(); +const address1 = accounts.get("wallet_1")!; +const address2 = accounts.get("wallet_2")!; +const addressDeployer = accounts.get("deployer")!; + +const contractAddress = `${addressDeployer}.aibtc-prop001-bootstrap`; + +describe("aibtc-prop001-bootstrap", () => { + // Manifest Tests + describe("get-dao-manifest()", () => { + it("returns DAO_MANIFEST as string"); + }); +});