diff --git a/Clarinet.toml b/Clarinet.toml index 53801053..242ff00f 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -103,60 +103,100 @@ path = 'contracts/dao/extensions/aibtc-onchain-messaging.clar' clarity_version = 3 epoch = 3.1 -[contracts.aibtc-payments-invoices] -path = 'contracts/dao/extensions/aibtc-payments-invoices.clar' -clarity_version = 2 +[contracts.aibtc-payment-processor-dao] +path = 'contracts/dao/extensions/aibtc-payment-processor-dao.clar' +clarity_version = 3 epoch = 3.1 -[contracts.aibtc-timed-vault] -path = 'contracts/dao/extensions/aibtc-timed-vault.clar' -clarity_version = 2 +[contracts.aibtc-payment-processor-sbtc] +path = 'contracts/dao/extensions/aibtc-payment-processor-sbtc.clar' +clarity_version = 3 +epoch = 3.1 + +[contracts.aibtc-payment-processor-stx] +path = 'contracts/dao/extensions/aibtc-payment-processor-stx.clar' +clarity_version = 3 +epoch = 3.1 + +[contracts.aibtc-timed-vault-dao] +path = 'contracts/dao/extensions/aibtc-timed-vault-dao.clar' +clarity_version = 3 +epoch = 3.1 + +[contracts.aibtc-timed-vault-sbtc] +path = 'contracts/dao/extensions/aibtc-timed-vault-sbtc.clar' +clarity_version = 3 +epoch = 3.1 + +[contracts.aibtc-timed-vault-stx] +path = 'contracts/dao/extensions/aibtc-timed-vault-stx.clar' +clarity_version = 3 epoch = 3.1 [contracts.aibtc-token-owner] path = 'contracts/dao/extensions/aibtc-token-owner.clar' -clarity_version = 2 +clarity_version = 3 epoch = 3.1 [contracts.aibtc-treasury] path = 'contracts/dao/extensions/aibtc-treasury.clar' -clarity_version = 2 +clarity_version = 3 epoch = 3.1 # dao actions (as extensions) -[contracts.aibtc-action-add-resource] -path = 'contracts/dao/extensions/actions/aibtc-action-add-resource.clar' +[contracts.aibtc-action-pmt-dao-add-resource] +path = 'contracts/dao/extensions/actions/aibtc-action-pmt-dao-add-resource.clar' clarity_version = 2 epoch = 3.1 -[contracts.aibtc-action-allow-asset] -path = 'contracts/dao/extensions/actions/aibtc-action-allow-asset.clar' +[contracts.aibtc-action-pmt-dao-toggle-resource] +path = 'contracts/dao/extensions/actions/aibtc-action-pmt-dao-toggle-resource.clar' clarity_version = 2 epoch = 3.1 -[contracts.aibtc-action-send-message] -path = 'contracts/dao/extensions/actions/aibtc-action-send-message.clar' +[contracts.aibtc-action-pmt-sbtc-add-resource] +path = 'contracts/dao/extensions/actions/aibtc-action-pmt-sbtc-add-resource.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-action-pmt-sbtc-toggle-resource] +path = 'contracts/dao/extensions/actions/aibtc-action-pmt-sbtc-toggle-resource.clar' clarity_version = 2 epoch = 3.1 -[contracts.aibtc-action-set-account-holder] -path = 'contracts/dao/extensions/actions/aibtc-action-set-account-holder.clar' +[contracts.aibtc-action-pmt-stx-add-resource] +path = 'contracts/dao/extensions/actions/aibtc-action-pmt-stx-add-resource.clar' clarity_version = 2 epoch = 3.1 -[contracts.aibtc-action-set-withdrawal-amount] -path = 'contracts/dao/extensions/actions/aibtc-action-set-withdrawal-amount.clar' +[contracts.aibtc-action-pmt-stx-toggle-resource] +path = 'contracts/dao/extensions/actions/aibtc-action-pmt-stx-toggle-resource.clar' clarity_version = 2 epoch = 3.1 -[contracts.aibtc-action-set-withdrawal-period] -path = 'contracts/dao/extensions/actions/aibtc-action-set-withdrawal-period.clar' +[contracts.aibtc-action-treasury-allow-asset] +path = 'contracts/dao/extensions/actions/aibtc-action-treasury-allow-asset.clar' clarity_version = 2 epoch = 3.1 -[contracts.aibtc-action-toggle-resource-by-name] -path = 'contracts/dao/extensions/actions/aibtc-action-toggle-resource-by-name.clar' +[contracts.aibtc-action-send-message] +path = 'contracts/dao/extensions/actions/aibtc-action-send-message.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-action-configure-timed-vault-dao] +path = 'contracts/dao/extensions/actions/aibtc-action-configure-timed-vault-dao.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-action-configure-timed-vault-sbtc] +path = 'contracts/dao/extensions/actions/aibtc-action-configure-timed-vault-sbtc.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-action-configure-timed-vault-stx] +path = 'contracts/dao/extensions/actions/aibtc-action-configure-timed-vault-stx.clar' clarity_version = 2 epoch = 3.1 @@ -167,33 +207,33 @@ path = 'contracts/dao/proposals/aibtc-action-proposals-set-proposal-bond.clar' clarity_version = 2 epoch = 3.1 -[contracts.aibtc-timed-vault-initialize-new-account] -path = 'contracts/dao/proposals/aibtc-timed-vault-initialize-new-account.clar' +[contracts.aibtc-timed-vault-stx-initialize-new-vault] +path = 'contracts/dao/proposals/aibtc-timed-vault-stx-initialize-new-vault.clar' clarity_version = 2 epoch = 3.1 -[contracts.aibtc-timed-vault-override-last-withdrawal-block] -path = 'contracts/dao/proposals/aibtc-timed-vault-override-last-withdrawal-block.clar' +[contracts.aibtc-timed-vault-stx-override-last-withdrawal-block] +path = 'contracts/dao/proposals/aibtc-timed-vault-stx-override-last-withdrawal-block.clar' clarity_version = 2 epoch = 3.1 -[contracts.aibtc-timed-vault-set-account-holder] -path = 'contracts/dao/proposals/aibtc-timed-vault-set-account-holder.clar' +[contracts.aibtc-timed-vault-stx-set-account-holder] +path = 'contracts/dao/proposals/aibtc-timed-vault-stx-set-account-holder.clar' clarity_version = 2 epoch = 3.1 -[contracts.aibtc-timed-vault-set-withdrawal-amount] -path = 'contracts/dao/proposals/aibtc-timed-vault-set-withdrawal-amount.clar' +[contracts.aibtc-timed-vault-stx-set-withdrawal-amount] +path = 'contracts/dao/proposals/aibtc-timed-vault-stx-set-withdrawal-amount.clar' clarity_version = 2 epoch = 3.1 -[contracts.aibtc-timed-vault-set-withdrawal-period] -path = 'contracts/dao/proposals/aibtc-timed-vault-set-withdrawal-period.clar' +[contracts.aibtc-timed-vault-stx-set-withdrawal-period] +path = 'contracts/dao/proposals/aibtc-timed-vault-stx-set-withdrawal-period.clar' clarity_version = 2 epoch = 3.1 -[contracts.aibtc-timed-vault-withdraw-stx] -path = 'contracts/dao/proposals/aibtc-timed-vault-withdraw-stx.clar' +[contracts.aibtc-timed-vault-stx-withdraw] +path = 'contracts/dao/proposals/aibtc-timed-vault-stx-withdraw.clar' clarity_version = 2 epoch = 3.1 @@ -247,23 +287,63 @@ path = 'contracts/dao/proposals/aibtc-onchain-messaging-send.clar' clarity_version = 2 epoch = 3.1 -[contracts.aibtc-payments-invoices-add-resource] -path = 'contracts/dao/proposals/aibtc-payments-invoices-add-resource.clar' +[contracts.aibtc-pmt-stx-add-resource] +path = 'contracts/dao/proposals/aibtc-pmt-stx-add-resource.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-pmt-stx-set-payment-address] +path = 'contracts/dao/proposals/aibtc-pmt-stx-set-payment-address.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-pmt-stx-toggle-resource-by-name] +path = 'contracts/dao/proposals/aibtc-pmt-stx-toggle-resource-by-name.clar' clarity_version = 2 epoch = 3.1 -[contracts.aibtc-payments-invoices-set-payment-address] -path = 'contracts/dao/proposals/aibtc-payments-invoices-set-payment-address.clar' +[contracts.aibtc-pmt-stx-toggle-resource] +path = 'contracts/dao/proposals/aibtc-pmt-stx-toggle-resource.clar' clarity_version = 2 epoch = 3.1 -[contracts.aibtc-payments-invoices-toggle-resource-by-name] -path = 'contracts/dao/proposals/aibtc-payments-invoices-toggle-resource-by-name.clar' +[contracts.aibtc-pmt-sbtc-add-resource] +path = 'contracts/dao/proposals/aibtc-pmt-sbtc-add-resource.clar' clarity_version = 2 epoch = 3.1 -[contracts.aibtc-payments-invoices-toggle-resource] -path = 'contracts/dao/proposals/aibtc-payments-invoices-toggle-resource.clar' +[contracts.aibtc-pmt-sbtc-set-payment-address] +path = 'contracts/dao/proposals/aibtc-pmt-sbtc-set-payment-address.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-pmt-sbtc-toggle-resource-by-name] +path = 'contracts/dao/proposals/aibtc-pmt-sbtc-toggle-resource-by-name.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-pmt-sbtc-toggle-resource] +path = 'contracts/dao/proposals/aibtc-pmt-sbtc-toggle-resource.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-pmt-dao-add-resource] +path = 'contracts/dao/proposals/aibtc-pmt-dao-add-resource.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-pmt-dao-set-payment-address] +path = 'contracts/dao/proposals/aibtc-pmt-dao-set-payment-address.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-pmt-dao-toggle-resource-by-name] +path = 'contracts/dao/proposals/aibtc-pmt-dao-toggle-resource-by-name.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-pmt-dao-toggle-resource] +path = 'contracts/dao/proposals/aibtc-pmt-dao-toggle-resource.clar' clarity_version = 2 epoch = 3.1 @@ -282,6 +362,11 @@ path = 'contracts/dao/proposals/aibtc-treasury-allow-asset.clar' clarity_version = 2 epoch = 3.1 +[contracts.aibtc-treasury-allow-assets] +path = 'contracts/dao/proposals/aibtc-treasury-allow-assets.clar' +clarity_version = 2 +epoch = 3.1 + [contracts.aibtc-treasury-delegate-stx] path = 'contracts/dao/proposals/aibtc-treasury-delegate-stx.clar' clarity_version = 2 @@ -312,8 +397,78 @@ path = 'contracts/dao/proposals/aibtc-treasury-withdraw-stx.clar' clarity_version = 2 epoch = 3.1 +[contracts.aibtc-timed-vault-sbtc-initialize-new-vault] +path = 'contracts/dao/proposals/aibtc-timed-vault-sbtc-initialize-new-vault.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-timed-vault-sbtc-override-last-withdrawal-block] +path = 'contracts/dao/proposals/aibtc-timed-vault-sbtc-override-last-withdrawal-block.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-timed-vault-sbtc-set-account-holder] +path = 'contracts/dao/proposals/aibtc-timed-vault-sbtc-set-account-holder.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-timed-vault-sbtc-set-withdrawal-amount] +path = 'contracts/dao/proposals/aibtc-timed-vault-sbtc-set-withdrawal-amount.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-timed-vault-sbtc-set-withdrawal-period] +path = 'contracts/dao/proposals/aibtc-timed-vault-sbtc-set-withdrawal-period.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-timed-vault-sbtc-withdraw] +path = 'contracts/dao/proposals/aibtc-timed-vault-sbtc-withdraw.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-timed-vault-dao-initialize-new-vault] +path = 'contracts/dao/proposals/aibtc-timed-vault-dao-initialize-new-vault.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-timed-vault-dao-override-last-withdrawal-block] +path = 'contracts/dao/proposals/aibtc-timed-vault-dao-override-last-withdrawal-block.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-timed-vault-dao-set-account-holder] +path = 'contracts/dao/proposals/aibtc-timed-vault-dao-set-account-holder.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-timed-vault-dao-set-withdrawal-amount] +path = 'contracts/dao/proposals/aibtc-timed-vault-dao-set-withdrawal-amount.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-timed-vault-dao-set-withdrawal-period] +path = 'contracts/dao/proposals/aibtc-timed-vault-dao-set-withdrawal-period.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-timed-vault-dao-withdraw] +path = 'contracts/dao/proposals/aibtc-timed-vault-dao-withdraw.clar' +clarity_version = 2 +epoch = 3.1 + # dao traits +[contracts.aibtc-dao-traits-v3] +path = 'contracts/dao/traits/aibtc-dao-traits-v3.clar' +clarity_version = 2 +epoch = 3.1 + +[contracts.aibtc-dao-v3] +path = 'contracts/dao/traits/aibtc-dao-v3.clar' +clarity_version = 2 +epoch = 3.1 + [contracts.aibtc-dao-traits-v2] path = 'contracts/dao/traits/aibtc-dao-traits-v2.clar' clarity_version = 2 diff --git a/codecov.yml b/codecov.yml index 7cc234a4..cef01409 100644 --- a/codecov.yml +++ b/codecov.yml @@ -5,12 +5,10 @@ coverage: precision: 2 round: down range: "70...100" - status: - status: project: default: - target: 50% # 95% goal + target: 60% # 95% goal threshold: 10% # 5% goal base: auto if_ci_failed: error @@ -32,4 +30,6 @@ comment: require_head: true ignore: - - "contracts/test/**/*" # Ignore everything in contracts/test directory + - "contracts/test" + - "aibtcdev-airdrop-1.clar" + - "aibtcdev-airdrop-2.clar" diff --git a/contracts/aibtc-smart-wallet-traits-v2.clar b/contracts/aibtc-smart-wallet-traits-v2.clar new file mode 100644 index 00000000..ca49cacf --- /dev/null +++ b/contracts/aibtc-smart-wallet-traits-v2.clar @@ -0,0 +1,103 @@ +;; title: aibtc-smart-wallet-traits +;; version: 2.0.0 +;; summary: A collection of traits for user agent smart wallets. + +;; IMPORTS +(use-trait sip010-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) +(use-trait dao-action-trait .aibtc-dao-traits-v3.action) +(use-trait dao-proposal-trait .aibtc-dao-traits-v3.proposal) +(use-trait dao-action-proposals-trait .aibtc-dao-traits-v3.action-proposals) +(use-trait dao-core-proposals-trait .aibtc-dao-traits-v3.core-proposals) +(use-trait dao-faktory-dex .aibtc-dao-traits-v3.faktory-dex) +(use-trait faktory-token .faktory-trait-v1.sip-010-trait) + +;; SMART WALLET TRAITS + +(define-trait aibtc-smart-wallet ( + ;; deposit STX to the smart wallet + ;; @param amount amount of microSTX to deposit + ;; @returns (response bool uint) + (deposit-stx (uint) (response bool uint)) + ;; deposit FT to the smart wallet + ;; @param ft the fungible token contract + ;; @param amount amount of tokens to deposit + ;; @returns (response bool uint) + (deposit-ft ( uint) (response bool uint)) + ;; withdraw STX from the smart wallet (user only) + ;; @param amount amount of microSTX to withdraw + ;; @returns (response bool uint) + (withdraw-stx (uint) (response bool uint)) + ;; withdraw FT from the smart wallet (user only) + ;; @param ft the fungible token contract + ;; @param amount amount of tokens to withdraw + ;; @returns (response bool uint) + (withdraw-ft ( uint) (response bool uint)) + ;; approve an asset for deposit/withdrawal (user only) + ;; @param asset the asset contract principal + ;; @returns (response bool uint) + (approve-asset (principal) (response bool uint)) + ;; revoke approval for an asset (user only) + ;; @param asset the asset contract principal + ;; @returns (response bool uint) + (revoke-asset (principal) (response bool uint)) +)) + +(define-trait aibtc-proposals-v3 ( + ;; propose an action to the DAO (user or agent) + ;; @param action-proposals the action proposals contract + ;; @param action the action contract + ;; @param parameters encoded action parameters + ;; @returns (response bool uint) + (proxy-propose-action ( (buff 2048) (optional (string-ascii 1024))) (response bool uint)) + ;; create a core proposal to the DAO (user or agent) + ;; @param core-proposals the core proposals contract + ;; @param proposal the proposal contract + ;; @returns (response bool uint) + (proxy-create-proposal ( (optional (string-ascii 1024))) (response bool uint)) + ;; vote on an action proposal (user or agent) + ;; @param action-proposals the action proposals contract + ;; @param proposalId the proposal ID + ;; @param vote true for yes, false for no + ;; @returns (response bool uint) + (vote-on-action-proposal ( uint bool) (response bool uint)) + ;; vote on a core proposal (user or agent) + ;; @param core-proposals the core proposals contract + ;; @param proposal the proposal contract + ;; @param vote true for yes, false for no + ;; @returns (response bool uint) + (vote-on-core-proposal ( bool) (response bool uint)) + ;; conclude an action proposal (user or agent) + ;; @param action-proposals the action proposals contract + ;; @param proposalId the proposal ID + ;; @param action the action contract + ;; @returns (response bool uint) + (conclude-action-proposal ( uint ) (response bool uint)) + ;; conclude a core proposal (user or agent) + ;; @param core-proposals the core proposals contract + ;; @param proposal the proposal contract + ;; @returns (response bool uint) + (conclude-core-proposal ( ) (response bool uint)) +)) + +(define-trait faktory-buy-sell ( + ;; buy an asset from a faktory dex + ;; @param faktory-dex the faktory dex contract + ;; @param asset the asset contract principal + ;; @param amount amount of tokens to buy + ;; @returns (response bool uint) + (buy-asset ( uint) (response bool uint)) + ;; sell an asset to a faktory dex + ;; @param faktory-dex the faktory dex contract + ;; @param asset the asset contract principal + ;; @param amount amount of tokens to sell + ;; @returns (response bool uint) + (sell-asset ( uint) (response bool uint)) + ;; approve a dex for trading an asset + ;; @param faktory-dex the faktory dex contract + ;; @returns (response bool uint) + (approve-dex () (response bool uint)) + ;; revoke approval for a dex + ;; @param faktory-dex the faktory dex contract + ;; @returns (response bool uint) + (revoke-dex () (response bool uint)) +)) diff --git a/contracts/aibtc-smart-wallet-traits.clar b/contracts/aibtc-smart-wallet-traits.clar index bc3a7363..6db1c25c 100644 --- a/contracts/aibtc-smart-wallet-traits.clar +++ b/contracts/aibtc-smart-wallet-traits.clar @@ -4,11 +4,11 @@ ;; IMPORTS (use-trait sip010-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) -(use-trait dao-action-trait .aibtc-dao-traits-v2.action) -(use-trait dao-proposal-trait .aibtc-dao-traits-v2.proposal) -(use-trait dao-action-proposals-trait .aibtc-dao-traits-v2.action-proposals) -(use-trait dao-core-proposals-trait .aibtc-dao-traits-v2.core-proposals) -(use-trait dao-faktory-dex .aibtc-dao-traits-v2.faktory-dex) +(use-trait dao-action-trait .aibtc-dao-traits-v3.action) +(use-trait dao-proposal-trait .aibtc-dao-traits-v3.proposal) +(use-trait dao-action-proposals-trait .aibtc-dao-traits-v3.action-proposals) +(use-trait dao-core-proposals-trait .aibtc-dao-traits-v3.core-proposals) +(use-trait dao-faktory-dex .aibtc-dao-traits-v3.faktory-dex) (use-trait faktory-token .faktory-trait-v1.sip-010-trait) ;; SMART WALLET TRAITS @@ -48,12 +48,12 @@ ;; @param action the action contract ;; @param parameters encoded action parameters ;; @returns (response bool uint) - (proxy-propose-action ( (buff 2048)) (response bool uint)) + (proxy-propose-action ( (buff 2048) (optional (string-ascii 1024))) (response bool uint)) ;; create a core proposal to the DAO (user or agent) ;; @param core-proposals the core proposals contract ;; @param proposal the proposal contract ;; @returns (response bool uint) - (proxy-create-proposal ( ) (response bool uint)) + (proxy-create-proposal ( (optional (string-ascii 1024))) (response bool uint)) ;; vote on an action proposal (user or agent) ;; @param action-proposals the action proposals contract ;; @param proposalId the proposal ID diff --git a/contracts/aibtc-user-agent-smart-wallet.clar b/contracts/aibtc-user-agent-smart-wallet.clar index b78c979a..4cf9d944 100644 --- a/contracts/aibtc-user-agent-smart-wallet.clar +++ b/contracts/aibtc-user-agent-smart-wallet.clar @@ -7,11 +7,11 @@ (impl-trait .aibtc-smart-wallet-traits.aibtc-proposals-v2) (impl-trait .aibtc-smart-wallet-traits.faktory-buy-sell) (use-trait ft-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) -(use-trait action-trait .aibtc-dao-traits-v2.action) -(use-trait proposal-trait .aibtc-dao-traits-v2.proposal) -(use-trait action-proposals-trait .aibtc-dao-traits-v2.action-proposals) -(use-trait core-proposals-trait .aibtc-dao-traits-v2.core-proposals) -(use-trait dao-faktory-dex .aibtc-dao-traits-v2.faktory-dex) +(use-trait action-trait .aibtc-dao-traits-v3.action) +(use-trait proposal-trait .aibtc-dao-traits-v3.proposal) +(use-trait action-proposals-trait .aibtc-dao-traits-v3.action-proposals) +(use-trait core-proposals-trait .aibtc-dao-traits-v3.core-proposals) +(use-trait dao-faktory-dex .aibtc-dao-traits-v3.faktory-dex) (use-trait faktory-token .faktory-trait-v1.sip-010-trait) ;; constants @@ -144,7 +144,7 @@ ;; DAO Interaction Functions -(define-public (proxy-propose-action (action-proposals ) (action ) (parameters (buff 2048))) +(define-public (proxy-propose-action (action-proposals ) (action ) (parameters (buff 2048)) (memo (optional (string-ascii 1024)))) (begin (asserts! (is-authorized) ERR_UNAUTHORIZED) (print { @@ -157,11 +157,11 @@ caller: contract-caller } }) - (as-contract (contract-call? action-proposals propose-action action parameters)) + (as-contract (contract-call? action-proposals propose-action action parameters memo)) ) ) -(define-public (proxy-create-proposal (core-proposals ) (proposal )) +(define-public (proxy-create-proposal (core-proposals ) (proposal ) (memo (optional (string-ascii 1024)))) (begin (asserts! (is-authorized) ERR_UNAUTHORIZED) (print { @@ -173,7 +173,7 @@ caller: contract-caller } }) - (as-contract (contract-call? core-proposals create-proposal proposal)) + (as-contract (contract-call? core-proposals create-proposal proposal memo)) ) ) diff --git a/contracts/dao/aibtc-base-dao.clar b/contracts/dao/aibtc-base-dao.clar index 80e6f0b9..9febccbb 100644 --- a/contracts/dao/aibtc-base-dao.clar +++ b/contracts/dao/aibtc-base-dao.clar @@ -6,8 +6,8 @@ ;; (impl-trait .aibtc-dao-v2.aibtc-base-dao) -(use-trait proposal-trait .aibtc-dao-traits-v2.proposal) -(use-trait extension-trait .aibtc-dao-traits-v2.extension) +(use-trait proposal-trait .aibtc-dao-traits-v3.proposal) +(use-trait extension-trait .aibtc-dao-traits-v3.extension) ;; constants ;; diff --git a/contracts/dao/extensions/actions/aibtc-action-configure-timed-vault-dao.clar b/contracts/dao/extensions/actions/aibtc-action-configure-timed-vault-dao.clar new file mode 100644 index 00000000..93a647f9 --- /dev/null +++ b/contracts/dao/extensions/actions/aibtc-action-configure-timed-vault-dao.clar @@ -0,0 +1,56 @@ +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.action) + +(define-constant ERR_UNAUTHORIZED (err u10001)) +(define-constant ERR_INVALID_PARAMS (err u10002)) + +(define-constant CFG_MESSAGE "Executed Action Proposal: Updated configuration in <%= it.dao_token_name %> timed vault extension") + +(define-public (callback (sender principal) (memo (buff 34))) (ok true)) + +(define-public (run (parameters (buff 2048))) + (let + ( + (paramsTuple (unwrap! (from-consensus-buff? + { accountHolder: (optional principal), amount: (optional uint), period: (optional uint) } + parameters) ERR_INVALID_PARAMS)) + (optAccountHolder (get accountHolder paramsTuple)) + (optAmount (get amount paramsTuple)) + (optPeriod (get period paramsTuple)) + ) + (try! (is-dao-or-extension)) + ;; have to provide at least one + (asserts! (or (is-some optAccountHolder) (is-some optAmount) (is-some optPeriod)) ERR_INVALID_PARAMS) + ;; send the message + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; set account holder if present + (and (is-some optAccountHolder) + (let ((accountHolder (unwrap! optAccountHolder ERR_INVALID_PARAMS))) + (try! (contract-call? .aibtc-timed-vault-dao set-account-holder accountHolder)) + ) + ) + ;; set amounts if present and within limits + (and (is-some optAmount) + (let ((amount (unwrap! optAmount ERR_INVALID_PARAMS))) + (asserts! (>= amount u100000000) ERR_INVALID_PARAMS) + (asserts! (<= amount u1000000000000) ERR_INVALID_PARAMS) + (try! (contract-call? .aibtc-timed-vault-dao set-withdrawal-amount amount)) + ) + ) + ;; set period if present and within limits + (and (is-some optPeriod) + (let ((period (unwrap! optPeriod ERR_INVALID_PARAMS))) + (asserts! (>= period u6) ERR_INVALID_PARAMS) + (asserts! (<= period u8064) ERR_INVALID_PARAMS) + (try! (contract-call? .aibtc-timed-vault-dao set-withdrawal-period period)) + ) + ) + (ok true) + ) +) + +(define-private (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .aibtc-base-dao) + (contract-call? .aibtc-base-dao is-extension contract-caller)) ERR_UNAUTHORIZED + )) +) diff --git a/contracts/dao/extensions/actions/aibtc-action-configure-timed-vault-sbtc.clar b/contracts/dao/extensions/actions/aibtc-action-configure-timed-vault-sbtc.clar new file mode 100644 index 00000000..72f884ca --- /dev/null +++ b/contracts/dao/extensions/actions/aibtc-action-configure-timed-vault-sbtc.clar @@ -0,0 +1,56 @@ +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.action) + +(define-constant ERR_UNAUTHORIZED (err u10001)) +(define-constant ERR_INVALID_PARAMS (err u10002)) + +(define-constant CFG_MESSAGE "Executed Action Proposal: Updated configuration in BTC timed vault extension") + +(define-public (callback (sender principal) (memo (buff 34))) (ok true)) + +(define-public (run (parameters (buff 2048))) + (let + ( + (paramsTuple (unwrap! (from-consensus-buff? + { accountHolder: (optional principal), amount: (optional uint), period: (optional uint) } + parameters) ERR_INVALID_PARAMS)) + (optAccountHolder (get accountHolder paramsTuple)) + (optAmount (get amount paramsTuple)) + (optPeriod (get period paramsTuple)) + ) + (try! (is-dao-or-extension)) + ;; have to provide at least one + (asserts! (or (is-some optAccountHolder) (is-some optAmount) (is-some optPeriod)) ERR_INVALID_PARAMS) + ;; send the message + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; set account holder if present + (and (is-some optAccountHolder) + (let ((accountHolder (unwrap! optAccountHolder ERR_INVALID_PARAMS))) + (try! (contract-call? .aibtc-timed-vault-sbtc set-account-holder accountHolder)) + ) + ) + ;; set amounts if present and within limits + (and (is-some optAmount) + (let ((amount (unwrap! optAmount ERR_INVALID_PARAMS))) + (asserts! (>= amount u1000) ERR_INVALID_PARAMS) + (asserts! (<= amount u10000000) ERR_INVALID_PARAMS) + (try! (contract-call? .aibtc-timed-vault-sbtc set-withdrawal-amount amount)) + ) + ) + ;; set period if present and within limits + (and (is-some optPeriod) + (let ((period (unwrap! optPeriod ERR_INVALID_PARAMS))) + (asserts! (>= period u6) ERR_INVALID_PARAMS) + (asserts! (<= period u8064) ERR_INVALID_PARAMS) + (try! (contract-call? .aibtc-timed-vault-sbtc set-withdrawal-period period)) + ) + ) + (ok true) + ) +) + +(define-private (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .aibtc-base-dao) + (contract-call? .aibtc-base-dao is-extension contract-caller)) ERR_UNAUTHORIZED + )) +) diff --git a/contracts/dao/extensions/actions/aibtc-action-configure-timed-vault-stx.clar b/contracts/dao/extensions/actions/aibtc-action-configure-timed-vault-stx.clar new file mode 100644 index 00000000..dce93dde --- /dev/null +++ b/contracts/dao/extensions/actions/aibtc-action-configure-timed-vault-stx.clar @@ -0,0 +1,56 @@ +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.action) + +(define-constant ERR_UNAUTHORIZED (err u10001)) +(define-constant ERR_INVALID_PARAMS (err u10002)) + +(define-constant CFG_MESSAGE "Executed Action Proposal: Updated configuration in STX timed vault extension") + +(define-public (callback (sender principal) (memo (buff 34))) (ok true)) + +(define-public (run (parameters (buff 2048))) + (let + ( + (paramsTuple (unwrap! (from-consensus-buff? + { accountHolder: (optional principal), amount: (optional uint), period: (optional uint) } + parameters) ERR_INVALID_PARAMS)) + (optAccountHolder (get accountHolder paramsTuple)) + (optAmount (get amount paramsTuple)) + (optPeriod (get period paramsTuple)) + ) + (try! (is-dao-or-extension)) + ;; have to provide at least one + (asserts! (or (is-some optAccountHolder) (is-some optAmount) (is-some optPeriod)) ERR_INVALID_PARAMS) + ;; send the message + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; set account holder if present + (and (is-some optAccountHolder) + (let ((accountHolder (unwrap! optAccountHolder ERR_INVALID_PARAMS))) + (try! (contract-call? .aibtc-timed-vault-stx set-account-holder accountHolder)) + ) + ) + ;; set amounts if present and within limits + (and (is-some optAmount) + (let ((amount (unwrap! optAmount ERR_INVALID_PARAMS))) + (asserts! (>= amount u1000000) ERR_INVALID_PARAMS) + (asserts! (<= amount u100000000) ERR_INVALID_PARAMS) + (try! (contract-call? .aibtc-timed-vault-stx set-withdrawal-amount amount)) + ) + ) + ;; set period if present and within limits + (and (is-some optPeriod) + (let ((period (unwrap! optPeriod ERR_INVALID_PARAMS))) + (asserts! (>= period u6) ERR_INVALID_PARAMS) + (asserts! (<= period u8064) ERR_INVALID_PARAMS) + (try! (contract-call? .aibtc-timed-vault-stx set-withdrawal-period period)) + ) + ) + (ok true) + ) +) + +(define-private (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .aibtc-base-dao) + (contract-call? .aibtc-base-dao is-extension contract-caller)) ERR_UNAUTHORIZED + )) +) diff --git a/contracts/dao/extensions/actions/aibtc-action-add-resource.clar b/contracts/dao/extensions/actions/aibtc-action-pmt-dao-add-resource.clar similarity index 73% rename from contracts/dao/extensions/actions/aibtc-action-add-resource.clar rename to contracts/dao/extensions/actions/aibtc-action-pmt-dao-add-resource.clar index 2d469460..e18b29f4 100644 --- a/contracts/dao/extensions/actions/aibtc-action-add-resource.clar +++ b/contracts/dao/extensions/actions/aibtc-action-pmt-dao-add-resource.clar @@ -1,10 +1,10 @@ -(impl-trait .aibtc-dao-traits-v2.extension) -(impl-trait .aibtc-dao-traits-v2.action) +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.action) (define-constant ERR_UNAUTHORIZED (err u10001)) (define-constant ERR_INVALID_PARAMS (err u10002)) -(define-constant CFG_MESSAGE "Executed Action Proposal: Added a resource in the payments/invoices extension") +(define-constant CFG_MESSAGE "Executed Action Proposal: Added a resource in the DAO payment processor extension") (define-public (callback (sender principal) (memo (buff 34))) (ok true)) @@ -17,7 +17,7 @@ ) (try! (is-dao-or-extension)) (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) - (try! (contract-call? .aibtc-payments-invoices add-resource (get name paramsTuple) (get description paramsTuple) (get price paramsTuple) (get url paramsTuple))) + (try! (contract-call? .aibtc-payment-processor-dao add-resource (get name paramsTuple) (get description paramsTuple) (get price paramsTuple) (get url paramsTuple))) (ok true) ) ) diff --git a/contracts/dao/extensions/actions/aibtc-action-toggle-resource-by-name.clar b/contracts/dao/extensions/actions/aibtc-action-pmt-dao-toggle-resource.clar similarity index 74% rename from contracts/dao/extensions/actions/aibtc-action-toggle-resource-by-name.clar rename to contracts/dao/extensions/actions/aibtc-action-pmt-dao-toggle-resource.clar index bb5f2f8e..133b870c 100644 --- a/contracts/dao/extensions/actions/aibtc-action-toggle-resource-by-name.clar +++ b/contracts/dao/extensions/actions/aibtc-action-pmt-dao-toggle-resource.clar @@ -1,10 +1,10 @@ -(impl-trait .aibtc-dao-traits-v2.extension) -(impl-trait .aibtc-dao-traits-v2.action) +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.action) (define-constant ERR_UNAUTHORIZED (err u10001)) (define-constant ERR_INVALID_PARAMS (err u10002)) -(define-constant CFG_MESSAGE "Executed Action Proposal: Toggled resource status by name in the payments/invoices extension") +(define-constant CFG_MESSAGE "Executed Action Proposal: Toggled resource status by name in the DAO payment processor extension") (define-public (callback (sender principal) (memo (buff 34))) (ok true)) @@ -15,7 +15,7 @@ ) (try! (is-dao-or-extension)) (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) - (contract-call? .aibtc-payments-invoices toggle-resource-by-name resourceName) + (contract-call? .aibtc-payment-processor-dao toggle-resource-by-name resourceName) ) ) diff --git a/contracts/dao/extensions/actions/aibtc-action-pmt-sbtc-add-resource.clar b/contracts/dao/extensions/actions/aibtc-action-pmt-sbtc-add-resource.clar new file mode 100644 index 00000000..df9fe171 --- /dev/null +++ b/contracts/dao/extensions/actions/aibtc-action-pmt-sbtc-add-resource.clar @@ -0,0 +1,29 @@ +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.action) + +(define-constant ERR_UNAUTHORIZED (err u10001)) +(define-constant ERR_INVALID_PARAMS (err u10002)) + +(define-constant CFG_MESSAGE "Executed Action Proposal: Added a resource in the BTC payment processor extension") + +(define-public (callback (sender principal) (memo (buff 34))) (ok true)) + +(define-public (run (parameters (buff 2048))) + (let + ( + (paramsTuple (unwrap! (from-consensus-buff? + { name: (string-utf8 50), description: (string-utf8 255), price: uint, url: (optional (string-utf8 255)) } + parameters) ERR_INVALID_PARAMS)) + ) + (try! (is-dao-or-extension)) + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + (try! (contract-call? .aibtc-payment-processor-sbtc add-resource (get name paramsTuple) (get description paramsTuple) (get price paramsTuple) (get url paramsTuple))) + (ok true) + ) +) + +(define-private (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .aibtc-base-dao) + (contract-call? .aibtc-base-dao is-extension contract-caller)) ERR_UNAUTHORIZED + )) +) diff --git a/contracts/dao/extensions/actions/aibtc-action-pmt-sbtc-toggle-resource.clar b/contracts/dao/extensions/actions/aibtc-action-pmt-sbtc-toggle-resource.clar new file mode 100644 index 00000000..505d8c9f --- /dev/null +++ b/contracts/dao/extensions/actions/aibtc-action-pmt-sbtc-toggle-resource.clar @@ -0,0 +1,26 @@ +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.action) + +(define-constant ERR_UNAUTHORIZED (err u10001)) +(define-constant ERR_INVALID_PARAMS (err u10002)) + +(define-constant CFG_MESSAGE "Executed Action Proposal: Toggled resource status by name in the BTC payment processor extension") + +(define-public (callback (sender principal) (memo (buff 34))) (ok true)) + +(define-public (run (parameters (buff 2048))) + (let + ( + (resourceName (unwrap! (from-consensus-buff? (string-utf8 50) parameters) ERR_INVALID_PARAMS)) + ) + (try! (is-dao-or-extension)) + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + (contract-call? .aibtc-payment-processor-sbtc toggle-resource-by-name resourceName) + ) +) + +(define-private (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .aibtc-base-dao) + (contract-call? .aibtc-base-dao is-extension contract-caller)) ERR_UNAUTHORIZED + )) +) diff --git a/contracts/dao/extensions/actions/aibtc-action-pmt-stx-add-resource.clar b/contracts/dao/extensions/actions/aibtc-action-pmt-stx-add-resource.clar new file mode 100644 index 00000000..baac0e90 --- /dev/null +++ b/contracts/dao/extensions/actions/aibtc-action-pmt-stx-add-resource.clar @@ -0,0 +1,29 @@ +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.action) + +(define-constant ERR_UNAUTHORIZED (err u10001)) +(define-constant ERR_INVALID_PARAMS (err u10002)) + +(define-constant CFG_MESSAGE "Executed Action Proposal: Added a resource in the STX payment processor extension") + +(define-public (callback (sender principal) (memo (buff 34))) (ok true)) + +(define-public (run (parameters (buff 2048))) + (let + ( + (paramsTuple (unwrap! (from-consensus-buff? + { name: (string-utf8 50), description: (string-utf8 255), price: uint, url: (optional (string-utf8 255)) } + parameters) ERR_INVALID_PARAMS)) + ) + (try! (is-dao-or-extension)) + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + (try! (contract-call? .aibtc-payment-processor-stx add-resource (get name paramsTuple) (get description paramsTuple) (get price paramsTuple) (get url paramsTuple))) + (ok true) + ) +) + +(define-private (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .aibtc-base-dao) + (contract-call? .aibtc-base-dao is-extension contract-caller)) ERR_UNAUTHORIZED + )) +) diff --git a/contracts/dao/extensions/actions/aibtc-action-set-account-holder.clar b/contracts/dao/extensions/actions/aibtc-action-pmt-stx-toggle-resource.clar similarity index 57% rename from contracts/dao/extensions/actions/aibtc-action-set-account-holder.clar rename to contracts/dao/extensions/actions/aibtc-action-pmt-stx-toggle-resource.clar index c32eee91..6372dcba 100644 --- a/contracts/dao/extensions/actions/aibtc-action-set-account-holder.clar +++ b/contracts/dao/extensions/actions/aibtc-action-pmt-stx-toggle-resource.clar @@ -1,21 +1,21 @@ -(impl-trait .aibtc-dao-traits-v2.extension) -(impl-trait .aibtc-dao-traits-v2.action) +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.action) (define-constant ERR_UNAUTHORIZED (err u10001)) (define-constant ERR_INVALID_PARAMS (err u10002)) -(define-constant CFG_MESSAGE "Executed Action Proposal: Set new account holder in timed vault extension") +(define-constant CFG_MESSAGE "Executed Action Proposal: Toggled resource status by name in the STX payment processor extension") (define-public (callback (sender principal) (memo (buff 34))) (ok true)) (define-public (run (parameters (buff 2048))) (let ( - (accountHolder (unwrap! (from-consensus-buff? principal parameters) ERR_INVALID_PARAMS)) + (resourceName (unwrap! (from-consensus-buff? (string-utf8 50) parameters) ERR_INVALID_PARAMS)) ) (try! (is-dao-or-extension)) (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) - (contract-call? .aibtc-timed-vault set-account-holder accountHolder) + (contract-call? .aibtc-payment-processor-stx toggle-resource-by-name resourceName) ) ) diff --git a/contracts/dao/extensions/actions/aibtc-action-send-message.clar b/contracts/dao/extensions/actions/aibtc-action-send-message.clar index a41b7fe3..d2647283 100644 --- a/contracts/dao/extensions/actions/aibtc-action-send-message.clar +++ b/contracts/dao/extensions/actions/aibtc-action-send-message.clar @@ -1,5 +1,5 @@ -(impl-trait .aibtc-dao-traits-v2.extension) -(impl-trait .aibtc-dao-traits-v2.action) +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.action) (define-constant ERR_UNAUTHORIZED (err u10001)) (define-constant ERR_INVALID_PARAMS (err u10002)) diff --git a/contracts/dao/extensions/actions/aibtc-action-set-withdrawal-amount.clar b/contracts/dao/extensions/actions/aibtc-action-set-withdrawal-amount.clar deleted file mode 100644 index 59056619..00000000 --- a/contracts/dao/extensions/actions/aibtc-action-set-withdrawal-amount.clar +++ /dev/null @@ -1,30 +0,0 @@ -(impl-trait .aibtc-dao-traits-v2.extension) -(impl-trait .aibtc-dao-traits-v2.action) - -(define-constant ERR_UNAUTHORIZED (err u10001)) -(define-constant ERR_INVALID_PARAMS (err u10002)) -(define-constant ERR_PARAMS_OUT_OF_RANGE (err u10003)) - -(define-constant CFG_MESSAGE "Executed Action Proposal: Set withdrawal amount in timed vault extension") - -(define-public (callback (sender principal) (memo (buff 34))) (ok true)) - -(define-public (run (parameters (buff 2048))) - (let - ( - (amount (unwrap! (from-consensus-buff? uint parameters) ERR_INVALID_PARAMS)) - ) - (try! (is-dao-or-extension)) - ;; verify within limits for low quorum - ;; more than 0, less than 100 STX (100_000_000) - (asserts! (and (> amount u0) (< amount u100000000)) ERR_INVALID_PARAMS) - (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) - (contract-call? .aibtc-timed-vault set-withdrawal-amount amount) - ) -) - -(define-private (is-dao-or-extension) - (ok (asserts! (or (is-eq tx-sender .aibtc-base-dao) - (contract-call? .aibtc-base-dao is-extension contract-caller)) ERR_UNAUTHORIZED - )) -) diff --git a/contracts/dao/extensions/actions/aibtc-action-set-withdrawal-period.clar b/contracts/dao/extensions/actions/aibtc-action-set-withdrawal-period.clar deleted file mode 100644 index d0f7cb93..00000000 --- a/contracts/dao/extensions/actions/aibtc-action-set-withdrawal-period.clar +++ /dev/null @@ -1,30 +0,0 @@ -(impl-trait .aibtc-dao-traits-v2.extension) -(impl-trait .aibtc-dao-traits-v2.action) - -(define-constant ERR_UNAUTHORIZED (err u10001)) -(define-constant ERR_INVALID_PARAMS (err u10002)) -(define-constant ERR_PARAMS_OUT_OF_RANGE (err u10003)) - -(define-constant CFG_MESSAGE "Executed Action Proposal: Set withdrawal period in timed vault extension") - -(define-public (callback (sender principal) (memo (buff 34))) (ok true)) - -(define-public (run (parameters (buff 2048))) - (let - ( - (period (unwrap! (from-consensus-buff? uint parameters) ERR_INVALID_PARAMS)) - ) - (try! (is-dao-or-extension)) - ;; verify within limits for low quorum - ;; more than 6 blocks (1hr), less than 1008 blocks (~1 week) - (asserts! (and (> period u6) (< period u1008)) ERR_PARAMS_OUT_OF_RANGE) - (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) - (contract-call? .aibtc-timed-vault set-withdrawal-period period) - ) -) - -(define-private (is-dao-or-extension) - (ok (asserts! (or (is-eq tx-sender .aibtc-base-dao) - (contract-call? .aibtc-base-dao is-extension contract-caller)) ERR_UNAUTHORIZED - )) -) diff --git a/contracts/dao/extensions/actions/aibtc-action-allow-asset.clar b/contracts/dao/extensions/actions/aibtc-action-treasury-allow-asset.clar similarity index 90% rename from contracts/dao/extensions/actions/aibtc-action-allow-asset.clar rename to contracts/dao/extensions/actions/aibtc-action-treasury-allow-asset.clar index 4fdadf81..740fdff1 100644 --- a/contracts/dao/extensions/actions/aibtc-action-allow-asset.clar +++ b/contracts/dao/extensions/actions/aibtc-action-treasury-allow-asset.clar @@ -1,5 +1,5 @@ -(impl-trait .aibtc-dao-traits-v2.extension) -(impl-trait .aibtc-dao-traits-v2.action) +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.action) (define-constant ERR_UNAUTHORIZED (err u10001)) (define-constant ERR_INVALID_PARAMS (err u10002)) diff --git a/contracts/dao/extensions/aibtc-action-proposals-v2.clar b/contracts/dao/extensions/aibtc-action-proposals-v2.clar index 6a735e67..69042219 100644 --- a/contracts/dao/extensions/aibtc-action-proposals-v2.clar +++ b/contracts/dao/extensions/aibtc-action-proposals-v2.clar @@ -5,11 +5,11 @@ ;; traits ;; -(impl-trait .aibtc-dao-traits-v2.extension) -(impl-trait .aibtc-dao-traits-v2.action-proposals) +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.action-proposals) -(use-trait action-trait .aibtc-dao-traits-v2.action) -(use-trait treasury-trait .aibtc-dao-traits-v2.treasury) +(use-trait action-trait .aibtc-dao-traits-v3.action) +(use-trait treasury-trait .aibtc-dao-traits-v3.treasury) ;; constants ;; @@ -52,6 +52,8 @@ ;; data vars ;; (define-data-var proposalCount uint u0) ;; total number of proposals +(define-data-var concludedProposalCount uint u0) ;; total number of concluded proposals +(define-data-var executedProposalCount uint u0) ;; total number of executed proposals (define-data-var lastProposalCreated uint u0) ;; block height of last proposal created (define-data-var proposalBond uint u100000000000) ;; proposal bond amount, starts at 1000 DAO tokens (8 decimals) @@ -65,6 +67,7 @@ createdAt: uint, ;; stacks block height for at-block calls caller: principal, ;; contract caller creator: principal, ;; proposal creator (tx-sender) + memo: (optional (string-ascii 1024)), ;; memo for the proposal bond: uint, ;; proposal bond amount startBlock: uint, ;; burn block height endBlock: uint, ;; burn block height @@ -113,7 +116,7 @@ ) ) -(define-public (propose-action (action ) (parameters (buff 2048))) +(define-public (propose-action (action ) (parameters (buff 2048)) (memo (optional (string-ascii 1024)))) (let ( (actionContract (contract-of action)) @@ -158,6 +161,7 @@ parameters: parameters, caller: contract-caller, creator: tx-sender, + memo: (if (is-some memo) memo none), bond: bondAmount, createdAt: createdAt, startBlock: startBlock, @@ -280,9 +284,12 @@ (try! (as-contract (contract-call? .aibtc-token transfer (get bond proposalRecord) SELF (get creator proposalRecord) none))) (try! (as-contract (contract-call? .aibtc-token transfer (get bond proposalRecord) SELF VOTING_TREASURY none))) ) + ;; increment the concluded proposal count + (var-set concludedProposalCount (+ (var-get concludedProposalCount) u1)) ;; execute the action only if it passed, return false if err (ok (if (and votePassed validAction notExpired) - (match (contract-call? action run (get parameters proposalRecord)) ok_ true err_ (begin (print {err:err_}) false)) + (and (var-set executedProposalCount (+ (var-get executedProposalCount) u1)) + (match (contract-call? action run (get parameters proposalRecord)) ok_ true err_ (begin (print {err:err_}) false))) false )) ) @@ -313,7 +320,11 @@ ) (define-read-only (get-total-proposals) - (var-get proposalCount) + { + total: (var-get proposalCount), + concluded: (var-get concludedProposalCount), + executed: (var-get executedProposalCount), + } ) (define-read-only (get-last-proposal-created) diff --git a/contracts/dao/extensions/aibtc-core-proposals-v2.clar b/contracts/dao/extensions/aibtc-core-proposals-v2.clar index 6b9f7041..abade68e 100644 --- a/contracts/dao/extensions/aibtc-core-proposals-v2.clar +++ b/contracts/dao/extensions/aibtc-core-proposals-v2.clar @@ -5,11 +5,11 @@ ;; traits ;; -(impl-trait .aibtc-dao-traits-v2.extension) -(impl-trait .aibtc-dao-traits-v2.core-proposals) +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.core-proposals) -(use-trait proposal-trait .aibtc-dao-traits-v2.proposal) -(use-trait treasury-trait .aibtc-dao-traits-v2.treasury) +(use-trait proposal-trait .aibtc-dao-traits-v3.proposal) +(use-trait treasury-trait .aibtc-dao-traits-v3.treasury) ;; constants ;; @@ -52,6 +52,8 @@ ;; data vars ;; (define-data-var proposalCount uint u0) ;; total number of proposals +(define-data-var concludedProposalCount uint u0) ;; total number of concluded proposals +(define-data-var executedProposalCount uint u0) ;; total number of executed proposals (define-data-var lastProposalCreated uint u0) ;; block height of last proposal created (define-data-var proposalBond uint u100000000000) ;; proposal bond amount, starts at 1000 DAO tokens (8 decimals) ;; data maps @@ -62,6 +64,7 @@ createdAt: uint, ;; stacks block height for at-block calls caller: principal, ;; contract caller creator: principal, ;; proposal creator (tx-sender) + memo: (optional (string-ascii 1024)), ;; memo for the proposal bond: uint, ;; proposal bond amount startBlock: uint, ;; burn block height endBlock: uint, ;; burn block height @@ -110,7 +113,7 @@ ) ) -(define-public (create-proposal (proposal )) +(define-public (create-proposal (proposal ) (memo (optional (string-ascii 1024)))) (let ( (proposalContract (contract-of proposal)) @@ -150,6 +153,7 @@ caller: contract-caller, creator: tx-sender, createdAt: createdAt, + memo: (if (is-some memo) memo none), bond: bondAmount, startBlock: startBlock, endBlock: endBlock, @@ -268,9 +272,12 @@ (try! (as-contract (contract-call? .aibtc-token transfer (get bond proposalRecord) SELF (get creator proposalRecord) none))) (try! (as-contract (contract-call? .aibtc-token transfer (get bond proposalRecord) SELF VOTING_TREASURY none))) ) + ;; increment the concluded proposal count + (var-set concludedProposalCount (+ (var-get concludedProposalCount) u1)) ;; execute the proposal only if it passed, return false if err (ok (if (and notExecuted notExpired votePassed) - (match (contract-call? .aibtc-base-dao execute proposal tx-sender) ok_ true err_ (begin (print {err:err_}) false)) + (and (var-set executedProposalCount (+ (var-get executedProposalCount) u1)) + (match (contract-call? .aibtc-base-dao execute proposal tx-sender) ok_ true err_ (begin (print {err:err_}) false))) false )) ) @@ -301,7 +308,11 @@ ) (define-read-only (get-total-proposals) - (var-get proposalCount) + { + total: (var-get proposalCount), + concluded: (var-get concludedProposalCount), + executed: (var-get executedProposalCount), + } ) (define-read-only (get-last-proposal-created) diff --git a/contracts/dao/extensions/aibtc-dao-charter.clar b/contracts/dao/extensions/aibtc-dao-charter.clar index dd2d77cd..9526a49f 100644 --- a/contracts/dao/extensions/aibtc-dao-charter.clar +++ b/contracts/dao/extensions/aibtc-dao-charter.clar @@ -6,8 +6,8 @@ ;; traits ;; -(impl-trait .aibtc-dao-traits-v2.extension) -(impl-trait .aibtc-dao-traits-v2.charter) +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.charter) ;; constants ;; diff --git a/contracts/dao/extensions/aibtc-onchain-messaging.clar b/contracts/dao/extensions/aibtc-onchain-messaging.clar index 80e91434..263eeefd 100644 --- a/contracts/dao/extensions/aibtc-onchain-messaging.clar +++ b/contracts/dao/extensions/aibtc-onchain-messaging.clar @@ -4,8 +4,8 @@ ;; traits ;; -(impl-trait .aibtc-dao-traits-v2.extension) -(impl-trait .aibtc-dao-traits-v2.messaging) +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.messaging) ;; constants ;; diff --git a/contracts/dao/extensions/aibtc-payment-processor-dao.clar b/contracts/dao/extensions/aibtc-payment-processor-dao.clar new file mode 100644 index 00000000..4cb6eb19 --- /dev/null +++ b/contracts/dao/extensions/aibtc-payment-processor-dao.clar @@ -0,0 +1,433 @@ +;; title: aibtc-payment-processor-dao +;; version: 1.0.0 +;; summary: An extension that provides payment processing for DAO services. + +;; traits +;; +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.invoices) +(impl-trait .aibtc-dao-traits-v3.resources) + +;; constants +;; + +;; initially scoped to service provider deploying a contract +(define-constant SELF (as-contract tx-sender)) +(define-constant CFG_PAYMENT_TOKEN .aibtc-token) + +;; errors +(define-constant ERR_NOT_DAO_OR_EXTENSION (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 +;; + +;; tracking counts for each map +(define-data-var userCount uint u0) +(define-data-var resourceCount uint u0) +(define-data-var invoiceCount uint u0) + +;; tracking overall contract revenue +(define-data-var totalRevenue uint u0) + +;; dao can update payment address +(define-data-var paymentAddress principal .aibtc-treasury) + +;; data maps +;; + +;; tracks user indexes by address +(define-map UserIndexes + principal ;; user address + uint ;; user index +) + +;; tracks full user data keyed by user index +;; can iterate over full map with userCount data-var +(define-map UserData + uint ;; user index + { + address: principal, + totalSpent: uint, + totalUsed: uint, + } +) + +;; tracks resource indexes by resource name +(define-map ResourceIndexes + (string-utf8 50) ;; resource name + uint ;; 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 + { + createdAt: uint, + enabled: bool, + name: (string-utf8 50), + description: (string-utf8 255), + price: uint, + totalSpent: uint, + totalUsed: uint, + url: (optional (string-utf8 255)), + } +) + +;; tracks invoices paid by users requesting access to a resource +(define-map InvoiceData + uint ;; invoice count + { + amount: uint, + createdAt: uint, + userIndex: uint, + resourceName: (string-utf8 50), + resourceIndex: uint, + } +) + +;; tracks last payment from user for a resource +(define-map RecentPayments + { + userIndex: uint, + resourceIndex: uint, + } + uint ;; invoice count +) + +;; public functions +;; + +(define-public (callback (sender principal) (memo (buff 34))) + (ok true) +) + +;; sets payment address used for invoices +(define-public (set-payment-address (newAddress principal)) + (begin + ;; 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_NOT_DAO_OR_EXTENSION) + ;; print details + (print { + notification: "set-payment-address", + payload: { + contractCaller: contract-caller, + oldAddress: (var-get paymentAddress), + newAddress: newAddress, + txSender: tx-sender, + } + }) + ;; set new payment address + (ok (var-set paymentAddress newAddress)) + ) +) + +;; adds active resource that invoices can be generated for +(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 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: burn-block-height, + enabled: true, + name: name, + description: description, + price: price, + totalSpent: u0, + totalUsed: u0, + url: url, + } + ) ERR_SAVING_RESOURCE_DATA) + ;; increment resourceCount + (var-set resourceCount newCount) + ;; print details + (print { + notification: "add-resource", + payload: { + contractCaller: contract-caller, + resourceData: (unwrap! (get-resource newCount) ERR_RESOURCE_NOT_FOUND), + resourceIndex: newCount, + txSender: tx-sender + } + }) + ;; return new count + (ok newCount) + ) +) + +;; toggles enabled status for resource +(define-public (toggle-resource (resourceIndex uint)) + (let + ( + (resourceData (unwrap! (get-resource resourceIndex) ERR_RESOURCE_NOT_FOUND)) + (newStatus (not (get enabled resourceData))) + ) + ;; verify resource > 0 + (asserts! (> resourceIndex u0) ERR_INVALID_PARAMS) + ;; check if caller is authorized + (try! (is-dao-or-extension)) + ;; update ResourceData map + (map-set ResourceData + resourceIndex + (merge resourceData { + enabled: newStatus + }) + ) + ;; print details + (print { + notification: "toggle-resource", + payload: { + resourceIndex: resourceIndex, + resourceData: (unwrap! (get-resource resourceIndex) ERR_RESOURCE_NOT_FOUND), + txSender: tx-sender, + contractCaller: contract-caller + } + }) + ;; return based on set status + (ok newStatus) + ) +) + +;; toggles enabled status for resource by name +(define-public (toggle-resource-by-name (name (string-utf8 50))) + (toggle-resource (unwrap! (get-resource-index name) ERR_RESOURCE_NOT_FOUND)) +) + +;; allows a user to pay an invoice for a resource +(define-public (pay-invoice (resourceIndex uint) (memo (optional (buff 34)))) + (let + ( + (newCount (+ (get-total-invoices) 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)) + ) + ;; check that resourceIndex is > 0 + (asserts! (> resourceIndex u0) ERR_INVALID_PARAMS) + ;; check that resource is enabled + (asserts! (get enabled resourceData) ERR_RESOURCE_DISABLED) + ;; update InvoiceData map + (asserts! (map-insert InvoiceData + newCount + { + amount: (get price resourceData), + createdAt: burn-block-height, + userIndex: userIndex, + resourceName: (get name resourceData), + resourceIndex: resourceIndex, + } + ) ERR_SAVING_INVOICE_DATA) + ;; update RecentPayments map + (map-set RecentPayments + { + userIndex: userIndex, + resourceIndex: resourceIndex, + } + newCount + ) + ;; update UserData map + (map-set UserData + userIndex + (merge userData { + totalSpent: (+ (get totalSpent userData) (get price resourceData)), + totalUsed: (+ (get totalUsed userData) u1) + }) + ) + ;; update ResourceData map + (map-set ResourceData + resourceIndex + (merge resourceData { + totalSpent: (+ (get totalSpent resourceData) (get price resourceData)), + totalUsed: (+ (get totalUsed resourceData) u1) + }) + ) + ;; update total revenue + (var-set totalRevenue (+ (var-get totalRevenue) (get price resourceData))) + ;; increment counter + (var-set invoiceCount newCount) + ;; print details + (print { + notification: "pay-invoice", + payload: { + 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), + resourceData: (unwrap! (get-resource resourceIndex) ERR_RESOURCE_NOT_FOUND), + resourceIndex: resourceIndex, + totalRevenue: (var-get totalRevenue), + txSender: tx-sender, + userIndex: userIndex, + userData: (unwrap! (get-user-data userIndex) ERR_USER_NOT_FOUND) + } + }) + ;; make transfer - directly pass the memo parameter + (try! (contract-call? .aibtc-token transfer (get price resourceData) contract-caller (var-get paymentAddress) memo)) + ;; return new count + (ok newCount) + ) +) + +(define-public (pay-invoice-by-resource-name (name (string-utf8 50)) (memo (optional (buff 34)))) + (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) + { + contractAddress: SELF, + paymentAddress: (get-payment-address), + paymentToken: CFG_PAYMENT_TOKEN, + totalInvoices: (get-total-invoices), + totalResources: (get-total-resources), + totalRevenue: (get-total-revenue), + totalUsers: (get-total-users) + } +) + +;; private functions +;; + +(define-private (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .aibtc-base-dao) + (contract-call? .aibtc-base-dao is-extension contract-caller)) ERR_NOT_DAO_OR_EXTENSION + )) +) + +(define-private (get-or-create-user (address principal)) + (match (map-get? UserIndexes address) + value (ok value) ;; return index if found + (let + ( + ;; increment current index + (newCount (+ (get-total-users) u1)) + ) + ;; update UserIndexes map, check address is unique + (asserts! (map-insert UserIndexes address newCount) ERR_USER_ALREADY_EXISTS) + ;; update UserData map + (asserts! (map-insert UserData + newCount + { + address: address, + totalSpent: u0, + totalUsed: u0, + } + ) ERR_SAVING_USER_DATA) + ;; save new index + (var-set userCount newCount) + ;; return new index + (ok newCount) + ) + ) +) diff --git a/contracts/dao/extensions/aibtc-payment-processor-sbtc.clar b/contracts/dao/extensions/aibtc-payment-processor-sbtc.clar new file mode 100644 index 00000000..be29eb3a --- /dev/null +++ b/contracts/dao/extensions/aibtc-payment-processor-sbtc.clar @@ -0,0 +1,433 @@ +;; title: aibtc-payment-processor-sbtc +;; version: 1.0.0 +;; summary: An extension that provides payment processing for DAO services. + +;; traits +;; +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.invoices) +(impl-trait .aibtc-dao-traits-v3.resources) + +;; constants +;; + +;; initially scoped to service provider deploying a contract +(define-constant SELF (as-contract tx-sender)) +(define-constant CFG_PAYMENT_TOKEN 'STV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RJ5XDY2.sbtc-token) + +;; errors +(define-constant ERR_NOT_DAO_OR_EXTENSION (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 +;; + +;; tracking counts for each map +(define-data-var userCount uint u0) +(define-data-var resourceCount uint u0) +(define-data-var invoiceCount uint u0) + +;; tracking overall contract revenue +(define-data-var totalRevenue uint u0) + +;; dao can update payment address +(define-data-var paymentAddress principal .aibtc-treasury) + +;; data maps +;; + +;; tracks user indexes by address +(define-map UserIndexes + principal ;; user address + uint ;; user index +) + +;; tracks full user data keyed by user index +;; can iterate over full map with userCount data-var +(define-map UserData + uint ;; user index + { + address: principal, + totalSpent: uint, + totalUsed: uint, + } +) + +;; tracks resource indexes by resource name +(define-map ResourceIndexes + (string-utf8 50) ;; resource name + uint ;; 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 + { + createdAt: uint, + enabled: bool, + name: (string-utf8 50), + description: (string-utf8 255), + price: uint, + totalSpent: uint, + totalUsed: uint, + url: (optional (string-utf8 255)), + } +) + +;; tracks invoices paid by users requesting access to a resource +(define-map InvoiceData + uint ;; invoice count + { + amount: uint, + createdAt: uint, + userIndex: uint, + resourceName: (string-utf8 50), + resourceIndex: uint, + } +) + +;; tracks last payment from user for a resource +(define-map RecentPayments + { + userIndex: uint, + resourceIndex: uint, + } + uint ;; invoice count +) + +;; public functions +;; + +(define-public (callback (sender principal) (memo (buff 34))) + (ok true) +) + +;; sets payment address used for invoices +(define-public (set-payment-address (newAddress principal)) + (begin + ;; 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_NOT_DAO_OR_EXTENSION) + ;; print details + (print { + notification: "set-payment-address", + payload: { + contractCaller: contract-caller, + oldAddress: (var-get paymentAddress), + newAddress: newAddress, + txSender: tx-sender, + } + }) + ;; set new payment address + (ok (var-set paymentAddress newAddress)) + ) +) + +;; adds active resource that invoices can be generated for +(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 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: burn-block-height, + enabled: true, + name: name, + description: description, + price: price, + totalSpent: u0, + totalUsed: u0, + url: url, + } + ) ERR_SAVING_RESOURCE_DATA) + ;; increment resourceCount + (var-set resourceCount newCount) + ;; print details + (print { + notification: "add-resource", + payload: { + contractCaller: contract-caller, + resourceData: (unwrap! (get-resource newCount) ERR_RESOURCE_NOT_FOUND), + resourceIndex: newCount, + txSender: tx-sender + } + }) + ;; return new count + (ok newCount) + ) +) + +;; toggles enabled status for resource +(define-public (toggle-resource (resourceIndex uint)) + (let + ( + (resourceData (unwrap! (get-resource resourceIndex) ERR_RESOURCE_NOT_FOUND)) + (newStatus (not (get enabled resourceData))) + ) + ;; verify resource > 0 + (asserts! (> resourceIndex u0) ERR_INVALID_PARAMS) + ;; check if caller is authorized + (try! (is-dao-or-extension)) + ;; update ResourceData map + (map-set ResourceData + resourceIndex + (merge resourceData { + enabled: newStatus + }) + ) + ;; print details + (print { + notification: "toggle-resource", + payload: { + resourceIndex: resourceIndex, + resourceData: (unwrap! (get-resource resourceIndex) ERR_RESOURCE_NOT_FOUND), + txSender: tx-sender, + contractCaller: contract-caller + } + }) + ;; return based on set status + (ok newStatus) + ) +) + +;; toggles enabled status for resource by name +(define-public (toggle-resource-by-name (name (string-utf8 50))) + (toggle-resource (unwrap! (get-resource-index name) ERR_RESOURCE_NOT_FOUND)) +) + +;; allows a user to pay an invoice for a resource +(define-public (pay-invoice (resourceIndex uint) (memo (optional (buff 34)))) + (let + ( + (newCount (+ (get-total-invoices) 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)) + ) + ;; check that resourceIndex is > 0 + (asserts! (> resourceIndex u0) ERR_INVALID_PARAMS) + ;; check that resource is enabled + (asserts! (get enabled resourceData) ERR_RESOURCE_DISABLED) + ;; update InvoiceData map + (asserts! (map-insert InvoiceData + newCount + { + amount: (get price resourceData), + createdAt: burn-block-height, + userIndex: userIndex, + resourceName: (get name resourceData), + resourceIndex: resourceIndex, + } + ) ERR_SAVING_INVOICE_DATA) + ;; update RecentPayments map + (map-set RecentPayments + { + userIndex: userIndex, + resourceIndex: resourceIndex, + } + newCount + ) + ;; update UserData map + (map-set UserData + userIndex + (merge userData { + totalSpent: (+ (get totalSpent userData) (get price resourceData)), + totalUsed: (+ (get totalUsed userData) u1) + }) + ) + ;; update ResourceData map + (map-set ResourceData + resourceIndex + (merge resourceData { + totalSpent: (+ (get totalSpent resourceData) (get price resourceData)), + totalUsed: (+ (get totalUsed resourceData) u1) + }) + ) + ;; update total revenue + (var-set totalRevenue (+ (var-get totalRevenue) (get price resourceData))) + ;; increment counter + (var-set invoiceCount newCount) + ;; print details + (print { + notification: "pay-invoice", + payload: { + 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), + resourceData: (unwrap! (get-resource resourceIndex) ERR_RESOURCE_NOT_FOUND), + resourceIndex: resourceIndex, + totalRevenue: (var-get totalRevenue), + txSender: tx-sender, + userIndex: userIndex, + userData: (unwrap! (get-user-data userIndex) ERR_USER_NOT_FOUND) + } + }) + ;; make transfer - directly pass the memo parameter + (try! (contract-call? 'STV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RJ5XDY2.sbtc-token transfer (get price resourceData) contract-caller (var-get paymentAddress) memo)) + ;; return new count + (ok newCount) + ) +) + +(define-public (pay-invoice-by-resource-name (name (string-utf8 50)) (memo (optional (buff 34)))) + (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) + { + contractAddress: SELF, + paymentAddress: (get-payment-address), + paymentToken: CFG_PAYMENT_TOKEN, + totalInvoices: (get-total-invoices), + totalResources: (get-total-resources), + totalRevenue: (get-total-revenue), + totalUsers: (get-total-users) + } +) + +;; private functions +;; + +(define-private (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .aibtc-base-dao) + (contract-call? .aibtc-base-dao is-extension contract-caller)) ERR_NOT_DAO_OR_EXTENSION + )) +) + +(define-private (get-or-create-user (address principal)) + (match (map-get? UserIndexes address) + value (ok value) ;; return index if found + (let + ( + ;; increment current index + (newCount (+ (get-total-users) u1)) + ) + ;; update UserIndexes map, check address is unique + (asserts! (map-insert UserIndexes address newCount) ERR_USER_ALREADY_EXISTS) + ;; update UserData map + (asserts! (map-insert UserData + newCount + { + address: address, + totalSpent: u0, + totalUsed: u0, + } + ) ERR_SAVING_USER_DATA) + ;; save new index + (var-set userCount newCount) + ;; return new index + (ok newCount) + ) + ) +) diff --git a/contracts/dao/extensions/aibtc-payments-invoices.clar b/contracts/dao/extensions/aibtc-payment-processor-stx.clar similarity index 96% rename from contracts/dao/extensions/aibtc-payments-invoices.clar rename to contracts/dao/extensions/aibtc-payment-processor-stx.clar index e5fe52bc..072d0a46 100644 --- a/contracts/dao/extensions/aibtc-payments-invoices.clar +++ b/contracts/dao/extensions/aibtc-payment-processor-stx.clar @@ -1,21 +1,22 @@ -;; title: aibtc-payments-invoices +;; title: aibtc-payment-processor-stx ;; version: 1.0.0 ;; summary: An extension that provides payment processing for DAO services. ;; traits ;; -(impl-trait .aibtc-dao-traits-v2.extension) -(impl-trait .aibtc-dao-traits-v2.invoices) -(impl-trait .aibtc-dao-traits-v2.resources) +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.invoices) +(impl-trait .aibtc-dao-traits-v3.resources) ;; constants ;; ;; initially scoped to service provider deploying a contract (define-constant SELF (as-contract tx-sender)) +(define-constant CFG_PAYMENT_TOKEN "STX") ;; errors -(define-constant ERR_UNAUTHORIZED (err u5000)) +(define-constant ERR_NOT_DAO_OR_EXTENSION (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)) @@ -120,7 +121,7 @@ ;; 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) + (asserts! (not (is-eq newAddress (var-get paymentAddress))) ERR_NOT_DAO_OR_EXTENSION) ;; print details (print { notification: "set-payment-address", @@ -189,10 +190,10 @@ (resourceData (unwrap! (get-resource resourceIndex) ERR_RESOURCE_NOT_FOUND)) (newStatus (not (get enabled resourceData))) ) - ;; verify resource > 0 - (asserts! (> resourceIndex u0) ERR_INVALID_PARAMS) ;; check if caller is authorized (try! (is-dao-or-extension)) + ;; verify resource > 0 + (asserts! (> resourceIndex u0) ERR_INVALID_PARAMS) ;; update ResourceData map (map-set ResourceData resourceIndex @@ -289,7 +290,7 @@ userData: (unwrap! (get-user-data userIndex) ERR_USER_NOT_FOUND) } }) - ;; make transfer + ;; make transfer - STX requires different functions for with/without memo (if (is-some memo) (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))) @@ -388,7 +389,9 @@ ;; returns aggregate contract data (define-read-only (get-contract-data) { + contractAddress: SELF, paymentAddress: (get-payment-address), + paymentToken: CFG_PAYMENT_TOKEN, totalInvoices: (get-total-invoices), totalResources: (get-total-resources), totalRevenue: (get-total-revenue), @@ -401,7 +404,7 @@ (define-private (is-dao-or-extension) (ok (asserts! (or (is-eq tx-sender .aibtc-base-dao) - (contract-call? .aibtc-base-dao is-extension contract-caller)) ERR_UNAUTHORIZED + (contract-call? .aibtc-base-dao is-extension contract-caller)) ERR_NOT_DAO_OR_EXTENSION )) ) diff --git a/contracts/dao/extensions/aibtc-timed-vault-dao.clar b/contracts/dao/extensions/aibtc-timed-vault-dao.clar new file mode 100644 index 00000000..5c760bf5 --- /dev/null +++ b/contracts/dao/extensions/aibtc-timed-vault-dao.clar @@ -0,0 +1,147 @@ +;; title: aibtc-timed-vault +;; version: 1.0.0 +;; summary: An extension that allows a principal to withdraw STX from the contract with given rules. + +;; traits +;; +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.timed-vault) + +;; constants +;; +(define-constant DEPLOYED_BURN_BLOCK burn-block-height) +(define-constant DEPLOYED_STACKS_BLOCK stacks-block-height) +(define-constant SELF (as-contract tx-sender)) + +;; template variables +(define-constant CFG_VAULT_TOKEN .aibtc-token) + +;; error messages +(define-constant ERR_NOT_DAO_OR_EXTENSION (err u2000)) +(define-constant ERR_INVALID (err u2001)) +(define-constant ERR_NOT_ACCOUNT_HOLDER (err u2002)) +(define-constant ERR_TOO_SOON (err u2003)) +(define-constant ERR_INVALID_AMOUNT (err u2004)) +(define-constant ERR_FETCHING_BALANCE (err u2005)) + + +;; data vars +;; +(define-data-var withdrawalPeriod uint u144) ;; 144 Bitcoin blocks, ~1 day +(define-data-var withdrawalAmount uint u100000000000) ;; 1,000 DAO token (8 decimals) +(define-data-var lastWithdrawalBlock uint u0) +(define-data-var accountHolder principal SELF) + + +;; public functions +;; + +(define-public (callback (sender principal) (memo (buff 34))) + (ok true) +) + +(define-public (set-account-holder (new principal)) + (begin + (try! (is-dao-or-extension)) + (asserts! (not (is-eq (var-get accountHolder) new)) ERR_INVALID) + (ok (var-set accountHolder new)) + ) +) + +(define-public (set-withdrawal-period (period uint)) + (begin + (try! (is-dao-or-extension)) + (asserts! (> period u0) ERR_INVALID) + (ok (var-set withdrawalPeriod period)) + ) +) + +(define-public (set-withdrawal-amount (amount uint)) + (begin + (try! (is-dao-or-extension)) + (asserts! (> amount u0) ERR_INVALID) + (ok (var-set withdrawalAmount amount)) + ) +) + +(define-public (override-last-withdrawal-block (block uint)) + (begin + (try! (is-dao-or-extension)) + (asserts! (> block DEPLOYED_BURN_BLOCK) ERR_INVALID) + (ok (var-set lastWithdrawalBlock block)) + ) +) + +(define-public (deposit (amount uint)) + (begin + (asserts! (> amount u0) ERR_INVALID_AMOUNT) + (print { + notification: "deposit", + payload: { + amount: amount, + caller: contract-caller, + recipient: SELF + } + }) + (contract-call? .aibtc-token transfer amount tx-sender SELF none) + ) +) + +(define-public (withdraw) + (begin + ;; verify user is enabled in the map + (try! (is-account-holder)) + ;; verify user is not withdrawing too soon + (asserts! (>= burn-block-height (+ (var-get lastWithdrawalBlock) (var-get withdrawalPeriod))) ERR_TOO_SOON) + ;; update last withdrawal block + (var-set lastWithdrawalBlock burn-block-height) + ;; print notification and transfer STX + (print { + notification: "withdraw", + payload: { + amount: (var-get withdrawalAmount), + caller: contract-caller, + recipient: (var-get accountHolder) + } + }) + (as-contract (contract-call? .aibtc-token transfer (var-get withdrawalAmount) SELF (var-get accountHolder) none)) + ) +) + +;; read only functions +;; +(define-read-only (get-account-balance) + (contract-call? .aibtc-token get-balance SELF) +) + +(define-read-only (get-account-terms) + { + accountHolder: (var-get accountHolder), + contractName: SELF, + deployedBurnBlock: DEPLOYED_BURN_BLOCK, + deployedStacksBlock: DEPLOYED_STACKS_BLOCK, + lastWithdrawalBlock: (var-get lastWithdrawalBlock), + vaultToken: CFG_VAULT_TOKEN, + withdrawalAmount: (var-get withdrawalAmount), + withdrawalPeriod: (var-get withdrawalPeriod), + } +) + +;; private functions +;; + +(define-private (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .aibtc-base-dao) + (contract-call? .aibtc-base-dao is-extension contract-caller)) ERR_NOT_DAO_OR_EXTENSION + )) +) + +(define-private (is-account-holder) + (ok (asserts! (is-eq (var-get accountHolder) (get-standard-caller)) ERR_NOT_ACCOUNT_HOLDER)) +) + +(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-timed-vault-sbtc.clar b/contracts/dao/extensions/aibtc-timed-vault-sbtc.clar new file mode 100644 index 00000000..87a80aa5 --- /dev/null +++ b/contracts/dao/extensions/aibtc-timed-vault-sbtc.clar @@ -0,0 +1,147 @@ +;; title: aibtc-timed-vault +;; version: 1.0.0 +;; summary: An extension that allows a principal to withdraw STX from the contract with given rules. + +;; traits +;; +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.timed-vault) + +;; constants +;; +(define-constant DEPLOYED_BURN_BLOCK burn-block-height) +(define-constant DEPLOYED_STACKS_BLOCK stacks-block-height) +(define-constant SELF (as-contract tx-sender)) + +;; template variables +(define-constant CFG_VAULT_TOKEN 'STV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RJ5XDY2.sbtc-token) + +;; error messages +(define-constant ERR_NOT_DAO_OR_EXTENSION (err u2000)) +(define-constant ERR_INVALID (err u2001)) +(define-constant ERR_NOT_ACCOUNT_HOLDER (err u2002)) +(define-constant ERR_TOO_SOON (err u2003)) +(define-constant ERR_INVALID_AMOUNT (err u2004)) +(define-constant ERR_FETCHING_BALANCE (err u2005)) + + +;; data vars +;; +(define-data-var withdrawalPeriod uint u144) ;; 144 Bitcoin blocks, ~1 day +(define-data-var withdrawalAmount uint u10000) ;; 10000 sats, or 0.0001 BTC +(define-data-var lastWithdrawalBlock uint u0) +(define-data-var accountHolder principal SELF) + + +;; public functions +;; + +(define-public (callback (sender principal) (memo (buff 34))) + (ok true) +) + +(define-public (set-account-holder (new principal)) + (begin + (try! (is-dao-or-extension)) + (asserts! (not (is-eq (var-get accountHolder) new)) ERR_INVALID) + (ok (var-set accountHolder new)) + ) +) + +(define-public (set-withdrawal-period (period uint)) + (begin + (try! (is-dao-or-extension)) + (asserts! (> period u0) ERR_INVALID) + (ok (var-set withdrawalPeriod period)) + ) +) + +(define-public (set-withdrawal-amount (amount uint)) + (begin + (try! (is-dao-or-extension)) + (asserts! (> amount u0) ERR_INVALID) + (ok (var-set withdrawalAmount amount)) + ) +) + +(define-public (override-last-withdrawal-block (block uint)) + (begin + (try! (is-dao-or-extension)) + (asserts! (> block DEPLOYED_BURN_BLOCK) ERR_INVALID) + (ok (var-set lastWithdrawalBlock block)) + ) +) + +(define-public (deposit (amount uint)) + (begin + (asserts! (> amount u0) ERR_INVALID_AMOUNT) + (print { + notification: "deposit", + payload: { + amount: amount, + caller: contract-caller, + recipient: SELF + } + }) + (contract-call? 'STV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RJ5XDY2.sbtc-token transfer amount tx-sender SELF none) + ) +) + +(define-public (withdraw) + (begin + ;; verify user is enabled in the map + (try! (is-account-holder)) + ;; verify user is not withdrawing too soon + (asserts! (>= burn-block-height (+ (var-get lastWithdrawalBlock) (var-get withdrawalPeriod))) ERR_TOO_SOON) + ;; update last withdrawal block + (var-set lastWithdrawalBlock burn-block-height) + ;; print notification and transfer sBTC + (print { + notification: "withdraw", + payload: { + amount: (var-get withdrawalAmount), + caller: contract-caller, + recipient: (var-get accountHolder) + } + }) + (as-contract (contract-call? 'STV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RJ5XDY2.sbtc-token transfer (var-get withdrawalAmount) SELF (var-get accountHolder) none)) + ) +) + +;; read only functions +;; +(define-read-only (get-account-balance) + (contract-call? 'STV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RJ5XDY2.sbtc-token get-balance SELF) +) + +(define-read-only (get-account-terms) + { + accountHolder: (var-get accountHolder), + contractName: SELF, + deployedBurnBlock: DEPLOYED_BURN_BLOCK, + deployedStacksBlock: DEPLOYED_STACKS_BLOCK, + lastWithdrawalBlock: (var-get lastWithdrawalBlock), + vaultToken: CFG_VAULT_TOKEN, + withdrawalAmount: (var-get withdrawalAmount), + withdrawalPeriod: (var-get withdrawalPeriod), + } +) + +;; private functions +;; + +(define-private (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .aibtc-base-dao) + (contract-call? .aibtc-base-dao is-extension contract-caller)) ERR_NOT_DAO_OR_EXTENSION + )) +) + +(define-private (is-account-holder) + (ok (asserts! (is-eq (var-get accountHolder) (get-standard-caller)) ERR_NOT_ACCOUNT_HOLDER)) +) + +(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-timed-vault.clar b/contracts/dao/extensions/aibtc-timed-vault-stx.clar similarity index 70% rename from contracts/dao/extensions/aibtc-timed-vault.clar rename to contracts/dao/extensions/aibtc-timed-vault-stx.clar index a0b1ac5d..0fd40352 100644 --- a/contracts/dao/extensions/aibtc-timed-vault.clar +++ b/contracts/dao/extensions/aibtc-timed-vault-stx.clar @@ -4,23 +4,27 @@ ;; traits ;; -(impl-trait .aibtc-dao-traits-v2.extension) -(impl-trait .aibtc-dao-traits-v2.timed-vault) +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.timed-vault) ;; constants ;; -(define-constant SELF (as-contract tx-sender)) (define-constant DEPLOYED_BURN_BLOCK 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)) +(define-constant DEPLOYED_STACKS_BLOCK stacks-block-height) +(define-constant SELF (as-contract tx-sender)) +(define-constant VAULT_TOKEN "STX") +;; error messages +(define-constant ERR_NOT_DAO_OR_EXTENSION (err u2000)) +(define-constant ERR_INVALID (err u2001)) +(define-constant ERR_NOT_ACCOUNT_HOLDER (err u2002)) +(define-constant ERR_TOO_SOON (err u2003)) +(define-constant ERR_INVALID_AMOUNT (err u2004)) ;; data vars ;; (define-data-var withdrawalPeriod uint u144) ;; 144 Bitcoin blocks, ~1 day -(define-data-var withdrawalAmount uint u10000000) ;; 10,000,000 microSTX, or 10 STX +(define-data-var withdrawalAmount uint u10000000) ;; 10 STX (6 decimals) (define-data-var lastWithdrawalBlock uint u0) (define-data-var accountHolder principal SELF) @@ -64,11 +68,11 @@ ) ) -(define-public (deposit-stx (amount uint)) +(define-public (deposit (amount uint)) (begin (asserts! (> amount u0) ERR_INVALID_AMOUNT) (print { - notification: "deposit-stx", + notification: "deposit", payload: { amount: amount, caller: contract-caller, @@ -79,7 +83,7 @@ ) ) -(define-public (withdraw-stx) +(define-public (withdraw) (begin ;; verify user is enabled in the map (try! (is-account-holder)) @@ -89,7 +93,7 @@ (var-set lastWithdrawalBlock burn-block-height) ;; print notification and transfer STX (print { - notification: "withdraw-stx", + notification: "withdraw", payload: { amount: (var-get withdrawalAmount), caller: contract-caller, @@ -102,39 +106,20 @@ ;; read only functions ;; -(define-read-only (get-deployed-block) - DEPLOYED_BURN_BLOCK -) - (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) -) - -(define-read-only (get-withdrawal-amount) - (var-get withdrawalAmount) + (ok (stx-get-balance SELF)) ) (define-read-only (get-account-terms) { - accountBalance: (get-account-balance), - accountHolder: (get-account-holder), + accountHolder: (var-get accountHolder), contractName: SELF, - deployedAt: (get-deployed-block), - lastWithdrawalBlock: (get-last-withdrawal-block), - withdrawalAmount: (get-withdrawal-amount), - withdrawalPeriod: (get-withdrawal-period), + deployedBurnBlock: DEPLOYED_BURN_BLOCK, + deployedStacksBlock: DEPLOYED_STACKS_BLOCK, + lastWithdrawalBlock: (var-get lastWithdrawalBlock), + vaultToken: VAULT_TOKEN, + withdrawalAmount: (var-get withdrawalAmount), + withdrawalPeriod: (var-get withdrawalPeriod), } ) @@ -143,12 +128,12 @@ (define-private (is-dao-or-extension) (ok (asserts! (or (is-eq tx-sender .aibtc-base-dao) - (contract-call? .aibtc-base-dao is-extension contract-caller)) ERR_UNAUTHORIZED + (contract-call? .aibtc-base-dao is-extension contract-caller)) ERR_NOT_DAO_OR_EXTENSION )) ) (define-private (is-account-holder) - (ok (asserts! (is-eq (var-get accountHolder) (get-standard-caller)) ERR_UNAUTHORIZED)) + (ok (asserts! (is-eq (var-get accountHolder) (get-standard-caller)) ERR_NOT_ACCOUNT_HOLDER)) ) (define-private (get-standard-caller) diff --git a/contracts/dao/extensions/aibtc-token-owner.clar b/contracts/dao/extensions/aibtc-token-owner.clar index 95eaeb6c..2ebc1524 100644 --- a/contracts/dao/extensions/aibtc-token-owner.clar +++ b/contracts/dao/extensions/aibtc-token-owner.clar @@ -4,8 +4,8 @@ ;; traits ;; -(impl-trait .aibtc-dao-traits-v2.extension) -(impl-trait .aibtc-dao-traits-v2.token-owner) +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.token-owner) ;; constants ;; diff --git a/contracts/dao/extensions/aibtc-treasury.clar b/contracts/dao/extensions/aibtc-treasury.clar index 59b13628..8b7ef175 100644 --- a/contracts/dao/extensions/aibtc-treasury.clar +++ b/contracts/dao/extensions/aibtc-treasury.clar @@ -4,8 +4,8 @@ ;; traits ;; -(impl-trait .aibtc-dao-traits-v2.extension) -(impl-trait .aibtc-dao-traits-v2.treasury) +(impl-trait .aibtc-dao-traits-v3.extension) +(impl-trait .aibtc-dao-traits-v3.treasury) (use-trait ft-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) (use-trait nft-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait) @@ -48,7 +48,8 @@ (define-public (allow-assets (allowList (list 100 {token: principal, enabled: bool}))) (begin (try! (is-dao-or-extension)) - (ok (map allow-assets-iter allowList)) + (map allow-assets-iter allowList) + (ok true) ) ) diff --git a/contracts/dao/proposals/aibtc-action-proposals-set-proposal-bond.clar b/contracts/dao/proposals/aibtc-action-proposals-set-proposal-bond.clar index a6a1c19a..18f26357 100644 --- a/contracts/dao/proposals/aibtc-action-proposals-set-proposal-bond.clar +++ b/contracts/dao/proposals/aibtc-action-proposals-set-proposal-bond.clar @@ -1,4 +1,4 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; diff --git a/contracts/dao/proposals/aibtc-base-add-new-extension.clar b/contracts/dao/proposals/aibtc-base-add-new-extension.clar index 0400d338..216799be 100644 --- a/contracts/dao/proposals/aibtc-base-add-new-extension.clar +++ b/contracts/dao/proposals/aibtc-base-add-new-extension.clar @@ -1,17 +1,17 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; (define-constant CFG_MESSAGE "Executed Core Proposal: Added new extension in the base DAO") ;; was CFG_MESSAGE_CONTRACT .aibtc-onchain-messaging ;; was CFG_BASE_DAO .aibtc-base-dao -;; was CFG_NEW_EXTENSION .aibtc-timed-vault +;; was CFG_NEW_EXTENSION .aibtc-timed-vault-stx (define-public (execute (sender principal)) (begin ;; send a message from the dao (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) ;; adds and enables a new extension to the DAO - (contract-call? .aibtc-base-dao set-extension .aibtc-timed-vault true) + (contract-call? .aibtc-base-dao set-extension .aibtc-timed-vault-stx true) ) ) diff --git a/contracts/dao/proposals/aibtc-base-bootstrap-initialization-v2.clar b/contracts/dao/proposals/aibtc-base-bootstrap-initialization-v2.clar index 3562c12e..6f833037 100644 --- a/contracts/dao/proposals/aibtc-base-bootstrap-initialization-v2.clar +++ b/contracts/dao/proposals/aibtc-base-bootstrap-initialization-v2.clar @@ -1,4 +1,4 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) (define-constant CFG_DAO_MANIFEST_TEXT "<%= it.dao_manifest %>") (define-constant CFG_DAO_MANIFEST_INSCRIPTION_ID "<%= it.dao_manifest_inscription_id %>") @@ -12,8 +12,12 @@ {extension: .aibtc-core-proposals-v2, enabled: true} {extension: .aibtc-dao-charter, enabled: true} {extension: .aibtc-onchain-messaging, enabled: true} - {extension: .aibtc-payments-invoices, enabled: true} - {extension: .aibtc-timed-vault, enabled: true} + {extension: .aibtc-payment-processor-dao, enabled: true} + {extension: .aibtc-payment-processor-sbtc, enabled: true} + {extension: .aibtc-payment-processor-stx, enabled: true} + {extension: .aibtc-timed-vault-dao, enabled: true} + {extension: .aibtc-timed-vault-sbtc, enabled: true} + {extension: .aibtc-timed-vault-stx, enabled: true} {extension: .aibtc-token-owner, enabled: true} {extension: .aibtc-treasury, enabled: true} ) @@ -21,13 +25,17 @@ ;; set initial action proposals list (try! (contract-call? .aibtc-base-dao set-extensions (list - {extension: .aibtc-action-add-resource, enabled: true} - {extension: .aibtc-action-allow-asset, enabled: true} + {extension: .aibtc-action-configure-timed-vault-dao, enabled: true} + {extension: .aibtc-action-configure-timed-vault-sbtc, enabled: true} + {extension: .aibtc-action-configure-timed-vault-stx, enabled: true} + {extension: .aibtc-action-pmt-dao-add-resource, enabled: true} + {extension: .aibtc-action-pmt-dao-toggle-resource, enabled: true} + {extension: .aibtc-action-pmt-sbtc-add-resource, enabled: true} + {extension: .aibtc-action-pmt-sbtc-toggle-resource, enabled: true} + {extension: .aibtc-action-pmt-stx-add-resource, enabled: true} + {extension: .aibtc-action-pmt-stx-toggle-resource, enabled: true} {extension: .aibtc-action-send-message, enabled: true} - {extension: .aibtc-action-set-account-holder, enabled: true} - {extension: .aibtc-action-set-withdrawal-amount, enabled: true} - {extension: .aibtc-action-set-withdrawal-period, enabled: true} - {extension: .aibtc-action-toggle-resource-by-name, enabled: true} + {extension: .aibtc-action-treasury-allow-asset, enabled: true} ) )) ;; set DAO manifest in dao-charter extension diff --git a/contracts/dao/proposals/aibtc-base-bootstrap-initialization.clar b/contracts/dao/proposals/aibtc-base-bootstrap-initialization.clar index afdf78a9..075f2e0b 100644 --- a/contracts/dao/proposals/aibtc-base-bootstrap-initialization.clar +++ b/contracts/dao/proposals/aibtc-base-bootstrap-initialization.clar @@ -8,7 +8,7 @@ (try! (contract-call? .aibtc-base-dao set-extensions (list {extension: .aibtc-action-proposals, enabled: true} - {extension: .aibtc-timed-vault, enabled: true} + {extension: .aibtc-timed-vault-stx, enabled: true} {extension: .aibtc-core-proposals, enabled: true} {extension: .aibtc-onchain-messaging, enabled: true} {extension: .aibtc-payments-invoices, enabled: true} @@ -20,11 +20,8 @@ (try! (contract-call? .aibtc-base-dao set-extensions (list {extension: .aibtc-action-add-resource, enabled: true} - {extension: .aibtc-action-allow-asset, enabled: true} + {extension: .aibtc-action-allow-treasury-asset, enabled: true} {extension: .aibtc-action-send-message, enabled: true} - {extension: .aibtc-action-set-account-holder, enabled: true} - {extension: .aibtc-action-set-withdrawal-amount, enabled: true} - {extension: .aibtc-action-set-withdrawal-period, enabled: true} {extension: .aibtc-action-toggle-resource-by-name, enabled: true} ) )) diff --git a/contracts/dao/proposals/aibtc-base-disable-extension.clar b/contracts/dao/proposals/aibtc-base-disable-extension.clar index 7107f5af..8b363e8d 100644 --- a/contracts/dao/proposals/aibtc-base-disable-extension.clar +++ b/contracts/dao/proposals/aibtc-base-disable-extension.clar @@ -1,11 +1,11 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; (define-constant CFG_MESSAGE "Executed Core Proposal: Disabled extension in the base DAO") ;; was CFG_MESSAGE_CONTRACT .aibtc-onchain-messaging ;; was CFG_BASE_DAO .aibtc-base-dao -;; was CFG_EXTENSION .aibtc-timed-vault +;; was CFG_EXTENSION .aibtc-timed-vault-stx ;; errors (define-constant ERR_EXTENSION_NOT_FOUND (err u3003)) @@ -15,9 +15,9 @@ ;; send a message from the dao (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) ;; check that extension exists, avoids write if not - (asserts! (contract-call? .aibtc-base-dao is-extension .aibtc-timed-vault) ERR_EXTENSION_NOT_FOUND) + (asserts! (contract-call? .aibtc-base-dao is-extension .aibtc-timed-vault-stx) ERR_EXTENSION_NOT_FOUND) ;; update extension status - (try! (contract-call? .aibtc-base-dao set-extension .aibtc-timed-vault false)) + (try! (contract-call? .aibtc-base-dao set-extension .aibtc-timed-vault-stx false)) (ok true) ) ) diff --git a/contracts/dao/proposals/aibtc-base-enable-extension.clar b/contracts/dao/proposals/aibtc-base-enable-extension.clar index ae6f975e..23896e6b 100644 --- a/contracts/dao/proposals/aibtc-base-enable-extension.clar +++ b/contracts/dao/proposals/aibtc-base-enable-extension.clar @@ -1,11 +1,11 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; (define-constant CFG_MESSAGE "Executed Core Proposal: Enabled extension in the base DAO") ;; was CFG_MESSAGE_CONTRACT .aibtc-onchain-messaging ;; was CFG_BASE_DAO .aibtc-base-dao -;; was CFG_EXTENSION .aibtc-timed-vault +;; was CFG_EXTENSION .aibtc-timed-vault-stx ;; errors (define-constant ERR_EXTENSION_NOT_FOUND (err u3003)) @@ -16,7 +16,7 @@ ;; send a message from the dao (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) ;; update extension status - (try! (contract-call? .aibtc-base-dao set-extension .aibtc-timed-vault true)) + (try! (contract-call? .aibtc-base-dao set-extension .aibtc-timed-vault-stx true)) (ok true) ) ) diff --git a/contracts/dao/proposals/aibtc-base-replace-extension-proposal-voting.clar b/contracts/dao/proposals/aibtc-base-replace-extension-proposal-voting.clar index 901de65e..14540aae 100644 --- a/contracts/dao/proposals/aibtc-base-replace-extension-proposal-voting.clar +++ b/contracts/dao/proposals/aibtc-base-replace-extension-proposal-voting.clar @@ -1,4 +1,4 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; diff --git a/contracts/dao/proposals/aibtc-base-replace-extension.clar b/contracts/dao/proposals/aibtc-base-replace-extension.clar index 8e7a9f02..19d379e1 100644 --- a/contracts/dao/proposals/aibtc-base-replace-extension.clar +++ b/contracts/dao/proposals/aibtc-base-replace-extension.clar @@ -1,12 +1,12 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; (define-constant CFG_MESSAGE "Executed Core Proposal: Replaced extension in the base DAO") ;; was CFG_MESSAGE_CONTRACT .aibtc-onchain-messaging ;; was CFG_BASE_DAO .aibtc-base-dao -;; was CFG_OLD_EXTENSION .aibtc-timed-vault -;; was CFG_NEW_EXTENSION .aibtc-timed-vault +;; was CFG_OLD_EXTENSION .aibtc-timed-vault-stx +;; was CFG_NEW_EXTENSION .aibtc-timed-vault-stx ;; errors (define-constant ERR_EXTENSION_NOT_FOUND (err u3003)) @@ -17,11 +17,11 @@ ;; send a message from the dao (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) ;; check that old extension exists - (asserts! (contract-call? .aibtc-base-dao is-extension .aibtc-timed-vault) ERR_EXTENSION_NOT_FOUND) + (asserts! (contract-call? .aibtc-base-dao is-extension .aibtc-timed-vault-stx) ERR_EXTENSION_NOT_FOUND) ;; update extension status to false - (try! (contract-call? .aibtc-base-dao set-extension .aibtc-timed-vault false)) + (try! (contract-call? .aibtc-base-dao set-extension .aibtc-timed-vault-stx false)) ;; add new extension to the dao - (try! (contract-call? .aibtc-base-dao set-extension .aibtc-timed-vault true)) + (try! (contract-call? .aibtc-base-dao set-extension .aibtc-timed-vault-stx true)) (ok true) ) ) diff --git a/contracts/dao/proposals/aibtc-core-proposals-set-proposal-bond.clar b/contracts/dao/proposals/aibtc-core-proposals-set-proposal-bond.clar index a35028c0..7f971ca4 100644 --- a/contracts/dao/proposals/aibtc-core-proposals-set-proposal-bond.clar +++ b/contracts/dao/proposals/aibtc-core-proposals-set-proposal-bond.clar @@ -1,4 +1,4 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; diff --git a/contracts/dao/proposals/aibtc-dao-charter-set-dao-charter.clar b/contracts/dao/proposals/aibtc-dao-charter-set-dao-charter.clar index bd24be02..3fa21025 100644 --- a/contracts/dao/proposals/aibtc-dao-charter-set-dao-charter.clar +++ b/contracts/dao/proposals/aibtc-dao-charter-set-dao-charter.clar @@ -1,4 +1,4 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; diff --git a/contracts/dao/proposals/aibtc-onchain-messaging-send.clar b/contracts/dao/proposals/aibtc-onchain-messaging-send.clar index 634b231e..5536c4c0 100644 --- a/contracts/dao/proposals/aibtc-onchain-messaging-send.clar +++ b/contracts/dao/proposals/aibtc-onchain-messaging-send.clar @@ -1,4 +1,4 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; diff --git a/contracts/dao/proposals/aibtc-payments-invoices-set-payment-address.clar b/contracts/dao/proposals/aibtc-payments-invoices-set-payment-address.clar deleted file mode 100644 index 92e7b6d4..00000000 --- a/contracts/dao/proposals/aibtc-payments-invoices-set-payment-address.clar +++ /dev/null @@ -1,17 +0,0 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) - -;; template vars -;; -(define-constant CFG_MESSAGE "Executed Core Proposal: Updated the payment address in the payments/invoices extension") -(define-constant CFG_PAYOUT_ADDRESS .aibtc-timed-vault) -;; was CFG_MESSAGE_CONTRACT .aibtc-onchain-messaging -;; was CFG_PAYMENTS_CONTRACT .aibtc-payments-invoices - -(define-public (execute (sender principal)) - (begin - ;; send a message from the dao - (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) - ;; set the payment address for invoices - (contract-call? .aibtc-payments-invoices set-payment-address CFG_PAYOUT_ADDRESS) - ) -) diff --git a/contracts/dao/proposals/aibtc-pmt-dao-add-resource.clar b/contracts/dao/proposals/aibtc-pmt-dao-add-resource.clar new file mode 100644 index 00000000..4375af39 --- /dev/null +++ b/contracts/dao/proposals/aibtc-pmt-dao-add-resource.clar @@ -0,0 +1,20 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Added a new resource available in the DAO payment processor") +(define-constant CFG_RESOURCE_NAME u"example-resource") +(define-constant CFG_RESOURCE_DESCRIPTION u"An example resource") +(define-constant CFG_RESOURCE_AMOUNT u100000000000) ;; 1,000 DAO tokens (8 decimals) +(define-constant CFG_RESOURCE_URL (some u"https://example.com")) ;; or none + +(define-public (execute (sender principal)) + ;; adds a resource that can be used to pay invoices + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; add a resource to the payments contract + (try! (contract-call? .aibtc-payment-processor-dao add-resource CFG_RESOURCE_NAME CFG_RESOURCE_DESCRIPTION CFG_RESOURCE_AMOUNT CFG_RESOURCE_URL)) + (ok true) + ) +) diff --git a/contracts/dao/proposals/aibtc-pmt-dao-set-payment-address.clar b/contracts/dao/proposals/aibtc-pmt-dao-set-payment-address.clar new file mode 100644 index 00000000..a7ed15e3 --- /dev/null +++ b/contracts/dao/proposals/aibtc-pmt-dao-set-payment-address.clar @@ -0,0 +1,15 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Updated the payment address in the DAO payment processor") +(define-constant CFG_PAYOUT_ADDRESS .aibtc-timed-vault-dao) + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; set the payment address for invoices + (contract-call? .aibtc-payment-processor-dao set-payment-address CFG_PAYOUT_ADDRESS) + ) +) diff --git a/contracts/dao/proposals/aibtc-payments-invoices-toggle-resource-by-name.clar b/contracts/dao/proposals/aibtc-pmt-dao-toggle-resource-by-name.clar similarity index 54% rename from contracts/dao/proposals/aibtc-payments-invoices-toggle-resource-by-name.clar rename to contracts/dao/proposals/aibtc-pmt-dao-toggle-resource-by-name.clar index 3f869d80..edf05fd9 100644 --- a/contracts/dao/proposals/aibtc-payments-invoices-toggle-resource-by-name.clar +++ b/contracts/dao/proposals/aibtc-pmt-dao-toggle-resource-by-name.clar @@ -1,17 +1,15 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; -(define-constant CFG_MESSAGE "Executed Core Proposal: Toggled a resource status by name in the payments/invoices extension") +(define-constant CFG_MESSAGE "Executed Core Proposal: Toggled a resource status by name in the DAO payment processor") (define-constant CFG_RESOURCE_NAME u"example-resource") -;; was CFG_MESSAGE_CONTRACT .aibtc-onchain-messaging -;; was CFG_PAYMENTS_CONTRACT .aibtc-payments-invoices (define-public (execute (sender principal)) (begin ;; send a message from the dao (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) ;; toggle a resource enabled status by name - (contract-call? .aibtc-payments-invoices toggle-resource-by-name CFG_RESOURCE_NAME) + (contract-call? .aibtc-payment-processor-dao toggle-resource-by-name CFG_RESOURCE_NAME) ) ) diff --git a/contracts/dao/proposals/aibtc-payments-invoices-toggle-resource.clar b/contracts/dao/proposals/aibtc-pmt-dao-toggle-resource.clar similarity index 54% rename from contracts/dao/proposals/aibtc-payments-invoices-toggle-resource.clar rename to contracts/dao/proposals/aibtc-pmt-dao-toggle-resource.clar index 18b1b92f..682bd077 100644 --- a/contracts/dao/proposals/aibtc-payments-invoices-toggle-resource.clar +++ b/contracts/dao/proposals/aibtc-pmt-dao-toggle-resource.clar @@ -1,17 +1,15 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; -(define-constant CFG_MESSAGE "Executed Core Proposal: Toggled a resource status by index in the payments/invoices extension") +(define-constant CFG_MESSAGE "Executed Core Proposal: Toggled a resource status by index in the DAO payment processor") (define-constant CFG_RESOURCE_INDEX u1) -;; was CFG_MESSAGE_CONTRACT .aibtc-onchain-messaging -;; was CFG_PAYMENTS_CONTRACT .aibtc-payments-invoices (define-public (execute (sender principal)) (begin ;; send a message from the dao (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) ;; toggle a resource enabled status by index - (contract-call? .aibtc-payments-invoices toggle-resource CFG_RESOURCE_INDEX) + (contract-call? .aibtc-payment-processor-dao toggle-resource CFG_RESOURCE_INDEX) ) ) diff --git a/contracts/dao/proposals/aibtc-pmt-sbtc-add-resource.clar b/contracts/dao/proposals/aibtc-pmt-sbtc-add-resource.clar new file mode 100644 index 00000000..bb1d494d --- /dev/null +++ b/contracts/dao/proposals/aibtc-pmt-sbtc-add-resource.clar @@ -0,0 +1,20 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Added a new resource available in the BTC payment processor") +(define-constant CFG_RESOURCE_NAME u"example-resource") +(define-constant CFG_RESOURCE_DESCRIPTION u"An example resource") +(define-constant CFG_RESOURCE_AMOUNT u1000000) ;; 0.01 BTC (8 decimals) +(define-constant CFG_RESOURCE_URL (some u"https://example.com")) ;; or none + +(define-public (execute (sender principal)) + ;; adds a resource that can be used to pay invoices + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; add a resource to the payments contract + (try! (contract-call? .aibtc-payment-processor-sbtc add-resource CFG_RESOURCE_NAME CFG_RESOURCE_DESCRIPTION CFG_RESOURCE_AMOUNT CFG_RESOURCE_URL)) + (ok true) + ) +) diff --git a/contracts/dao/proposals/aibtc-pmt-sbtc-set-payment-address.clar b/contracts/dao/proposals/aibtc-pmt-sbtc-set-payment-address.clar new file mode 100644 index 00000000..eefc9962 --- /dev/null +++ b/contracts/dao/proposals/aibtc-pmt-sbtc-set-payment-address.clar @@ -0,0 +1,15 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Updated the payment address in the BTC payment processor") +(define-constant CFG_PAYOUT_ADDRESS .aibtc-timed-vault-sbtc) + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; set the payment address for invoices + (contract-call? .aibtc-payment-processor-sbtc set-payment-address CFG_PAYOUT_ADDRESS) + ) +) diff --git a/contracts/dao/proposals/aibtc-pmt-sbtc-toggle-resource-by-name.clar b/contracts/dao/proposals/aibtc-pmt-sbtc-toggle-resource-by-name.clar new file mode 100644 index 00000000..4d141730 --- /dev/null +++ b/contracts/dao/proposals/aibtc-pmt-sbtc-toggle-resource-by-name.clar @@ -0,0 +1,15 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Toggled a resource status by name in the BTC payment processor") +(define-constant CFG_RESOURCE_NAME u"example-resource") + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; toggle a resource enabled status by name + (contract-call? .aibtc-payment-processor-sbtc toggle-resource-by-name CFG_RESOURCE_NAME) + ) +) diff --git a/contracts/dao/proposals/aibtc-pmt-sbtc-toggle-resource.clar b/contracts/dao/proposals/aibtc-pmt-sbtc-toggle-resource.clar new file mode 100644 index 00000000..49c5a627 --- /dev/null +++ b/contracts/dao/proposals/aibtc-pmt-sbtc-toggle-resource.clar @@ -0,0 +1,15 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Toggled a resource status by index in the BTC payment processor") +(define-constant CFG_RESOURCE_INDEX u1) + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; toggle a resource enabled status by index + (contract-call? .aibtc-payment-processor-sbtc toggle-resource CFG_RESOURCE_INDEX) + ) +) diff --git a/contracts/dao/proposals/aibtc-payments-invoices-add-resource.clar b/contracts/dao/proposals/aibtc-pmt-stx-add-resource.clar similarity index 63% rename from contracts/dao/proposals/aibtc-payments-invoices-add-resource.clar rename to contracts/dao/proposals/aibtc-pmt-stx-add-resource.clar index efdb6901..25e7458f 100644 --- a/contracts/dao/proposals/aibtc-payments-invoices-add-resource.clar +++ b/contracts/dao/proposals/aibtc-pmt-stx-add-resource.clar @@ -1,14 +1,12 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; -(define-constant CFG_MESSAGE "Executed Core Proposal: Added a new resource available in the payments/invoices extension") +(define-constant CFG_MESSAGE "Executed Core Proposal: Added a new resource available in the STX payment processor") (define-constant CFG_RESOURCE_NAME u"example-resource") (define-constant CFG_RESOURCE_DESCRIPTION u"An example resource") (define-constant CFG_RESOURCE_AMOUNT u1000000) (define-constant CFG_RESOURCE_URL (some u"https://example.com")) ;; or none -;; was CFG_MESSAGE_CONTRACT .aibtc-onchain-messaging -;; was CFG_PAYMENTS_CONTRACT .aibtc-payments-invoices (define-public (execute (sender principal)) ;; adds a resource that can be used to pay invoices @@ -16,7 +14,7 @@ ;; send a message from the dao (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) ;; add a resource to the payments contract - (try! (contract-call? .aibtc-payments-invoices add-resource CFG_RESOURCE_NAME CFG_RESOURCE_DESCRIPTION CFG_RESOURCE_AMOUNT CFG_RESOURCE_URL)) + (try! (contract-call? .aibtc-payment-processor-stx add-resource CFG_RESOURCE_NAME CFG_RESOURCE_DESCRIPTION CFG_RESOURCE_AMOUNT CFG_RESOURCE_URL)) (ok true) ) ) diff --git a/contracts/dao/proposals/aibtc-pmt-stx-set-payment-address.clar b/contracts/dao/proposals/aibtc-pmt-stx-set-payment-address.clar new file mode 100644 index 00000000..7b4e6c9f --- /dev/null +++ b/contracts/dao/proposals/aibtc-pmt-stx-set-payment-address.clar @@ -0,0 +1,15 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Updated the payment address in the STX payment processor") +(define-constant CFG_PAYOUT_ADDRESS .aibtc-timed-vault-stx) + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; set the payment address for invoices + (contract-call? .aibtc-payment-processor-stx set-payment-address CFG_PAYOUT_ADDRESS) + ) +) diff --git a/contracts/dao/proposals/aibtc-pmt-stx-toggle-resource-by-name.clar b/contracts/dao/proposals/aibtc-pmt-stx-toggle-resource-by-name.clar new file mode 100644 index 00000000..decc9ab4 --- /dev/null +++ b/contracts/dao/proposals/aibtc-pmt-stx-toggle-resource-by-name.clar @@ -0,0 +1,15 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Toggled a resource status by name in the STX payment processor") +(define-constant CFG_RESOURCE_NAME u"example-resource") + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; toggle a resource enabled status by name + (contract-call? .aibtc-payment-processor-stx toggle-resource-by-name CFG_RESOURCE_NAME) + ) +) diff --git a/contracts/dao/proposals/aibtc-pmt-stx-toggle-resource.clar b/contracts/dao/proposals/aibtc-pmt-stx-toggle-resource.clar new file mode 100644 index 00000000..f8e70164 --- /dev/null +++ b/contracts/dao/proposals/aibtc-pmt-stx-toggle-resource.clar @@ -0,0 +1,15 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Toggled a resource status by index in the STX payment processor") +(define-constant CFG_RESOURCE_INDEX u1) + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; toggle a resource enabled status by index + (contract-call? .aibtc-payment-processor-stx toggle-resource CFG_RESOURCE_INDEX) + ) +) diff --git a/contracts/dao/proposals/aibtc-timed-vault-dao-initialize-new-vault.clar b/contracts/dao/proposals/aibtc-timed-vault-dao-initialize-new-vault.clar new file mode 100644 index 00000000..7c298f99 --- /dev/null +++ b/contracts/dao/proposals/aibtc-timed-vault-dao-initialize-new-vault.clar @@ -0,0 +1,22 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Initialized a new DAO token timed vault in the base dao and funded it from the treasury") +(define-constant CFG_ACCOUNT_HOLDER 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5) +(define-constant CFG_AMOUNT_TO_FUND_DAO u100) ;; set to 0 to skip, in microDAO tokens + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; set the account holder in the timed vault + (try! (contract-call? .aibtc-timed-vault-dao set-account-holder CFG_ACCOUNT_HOLDER)) + ;; enable the extension in the dao + (try! (contract-call? .aibtc-base-dao set-extension .aibtc-timed-vault-dao true)) + ;; fund the extension from the treasury + (and (> CFG_AMOUNT_TO_FUND_DAO u0) + (try! (contract-call? .aibtc-treasury withdraw-ft .aibtc-token CFG_AMOUNT_TO_FUND_DAO .aibtc-timed-vault-dao))) + (ok true) + ) +) diff --git a/contracts/dao/proposals/aibtc-timed-vault-dao-override-last-withdrawal-block.clar b/contracts/dao/proposals/aibtc-timed-vault-dao-override-last-withdrawal-block.clar new file mode 100644 index 00000000..88c3ccd5 --- /dev/null +++ b/contracts/dao/proposals/aibtc-timed-vault-dao-override-last-withdrawal-block.clar @@ -0,0 +1,16 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Overwrote last withdrawal block in the DAO token timed vault extension") +;; add 5 to avoid matching deployed height of contracts for testing +(define-constant CFG_LAST_WITHDRAWAL_BLOCK (+ burn-block-height u5)) + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; override last withdrawal block in the timed vault + (contract-call? .aibtc-timed-vault-dao override-last-withdrawal-block CFG_LAST_WITHDRAWAL_BLOCK) + ) +) diff --git a/contracts/dao/proposals/aibtc-timed-vault-dao-set-account-holder.clar b/contracts/dao/proposals/aibtc-timed-vault-dao-set-account-holder.clar new file mode 100644 index 00000000..bd5ae9e6 --- /dev/null +++ b/contracts/dao/proposals/aibtc-timed-vault-dao-set-account-holder.clar @@ -0,0 +1,15 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Set or updated the account holder in the DAO token timed vault extension") +(define-constant CFG_ACCOUNT_HOLDER 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM) + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; set the account holder + (contract-call? .aibtc-timed-vault-dao set-account-holder CFG_ACCOUNT_HOLDER) + ) +) diff --git a/contracts/dao/proposals/aibtc-timed-vault-dao-set-withdrawal-amount.clar b/contracts/dao/proposals/aibtc-timed-vault-dao-set-withdrawal-amount.clar new file mode 100644 index 00000000..be5e36cf --- /dev/null +++ b/contracts/dao/proposals/aibtc-timed-vault-dao-set-withdrawal-amount.clar @@ -0,0 +1,15 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Set withdrawal amount in the DAO token timed vault extension") +(define-constant CFG_WITHDRAWAL_AMOUNT u1000000000) ;; 10 DAO tokens (8 decimals) + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; set the withdrawal amount + (contract-call? .aibtc-timed-vault-dao set-withdrawal-amount CFG_WITHDRAWAL_AMOUNT) + ) +) diff --git a/contracts/dao/proposals/aibtc-timed-vault-dao-set-withdrawal-period.clar b/contracts/dao/proposals/aibtc-timed-vault-dao-set-withdrawal-period.clar new file mode 100644 index 00000000..9b2206df --- /dev/null +++ b/contracts/dao/proposals/aibtc-timed-vault-dao-set-withdrawal-period.clar @@ -0,0 +1,15 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Set withdrawal period in the DAO token timed vault extension") +(define-constant CFG_WITHDRAWAL_PERIOD u144) + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; set the withdrawal period + (contract-call? .aibtc-timed-vault-dao set-withdrawal-period CFG_WITHDRAWAL_PERIOD) + ) +) diff --git a/contracts/dao/proposals/aibtc-timed-vault-dao-withdraw.clar b/contracts/dao/proposals/aibtc-timed-vault-dao-withdraw.clar new file mode 100644 index 00000000..64bc948b --- /dev/null +++ b/contracts/dao/proposals/aibtc-timed-vault-dao-withdraw.clar @@ -0,0 +1,14 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Withdrew DAO tokens from the timed vault extension") + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; withdraw DAO tokens from the timed vault + (contract-call? .aibtc-timed-vault-dao withdraw) + ) +) diff --git a/contracts/dao/proposals/aibtc-timed-vault-sbtc-initialize-new-vault.clar b/contracts/dao/proposals/aibtc-timed-vault-sbtc-initialize-new-vault.clar new file mode 100644 index 00000000..eb147960 --- /dev/null +++ b/contracts/dao/proposals/aibtc-timed-vault-sbtc-initialize-new-vault.clar @@ -0,0 +1,22 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Initialized a new BTC timed vault in the base dao and funded it from the treasury") +(define-constant CFG_ACCOUNT_HOLDER 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5) +(define-constant CFG_AMOUNT_TO_FUND_SBTC u100) ;; set to 0 to skip, in microsBTC + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; set the account holder in the timed vault + (try! (contract-call? .aibtc-timed-vault-sbtc set-account-holder CFG_ACCOUNT_HOLDER)) + ;; enable the extension in the dao + (try! (contract-call? .aibtc-base-dao set-extension .aibtc-timed-vault-sbtc true)) + ;; fund the extension from the treasury + (and (> CFG_AMOUNT_TO_FUND_SBTC u0) + (try! (contract-call? .aibtc-treasury withdraw-ft 'STV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RJ5XDY2.sbtc-token CFG_AMOUNT_TO_FUND_SBTC .aibtc-timed-vault-sbtc))) + (ok true) + ) +) diff --git a/contracts/dao/proposals/aibtc-timed-vault-sbtc-override-last-withdrawal-block.clar b/contracts/dao/proposals/aibtc-timed-vault-sbtc-override-last-withdrawal-block.clar new file mode 100644 index 00000000..fc81e884 --- /dev/null +++ b/contracts/dao/proposals/aibtc-timed-vault-sbtc-override-last-withdrawal-block.clar @@ -0,0 +1,16 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Overwrote last withdrawal block in the BTC timed vault extension") +;; add 5 to avoid matching deployed height of contracts for testing +(define-constant CFG_LAST_WITHDRAWAL_BLOCK (+ burn-block-height u5)) + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; override last withdrawal block in the timed vault + (contract-call? .aibtc-timed-vault-sbtc override-last-withdrawal-block CFG_LAST_WITHDRAWAL_BLOCK) + ) +) diff --git a/contracts/dao/proposals/aibtc-timed-vault-sbtc-set-account-holder.clar b/contracts/dao/proposals/aibtc-timed-vault-sbtc-set-account-holder.clar new file mode 100644 index 00000000..f1914f83 --- /dev/null +++ b/contracts/dao/proposals/aibtc-timed-vault-sbtc-set-account-holder.clar @@ -0,0 +1,15 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Set or updated the account holder in the BTC timed vault extension") +(define-constant CFG_ACCOUNT_HOLDER 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM) + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; set the account holder + (contract-call? .aibtc-timed-vault-sbtc set-account-holder CFG_ACCOUNT_HOLDER) + ) +) diff --git a/contracts/dao/proposals/aibtc-timed-vault-sbtc-set-withdrawal-amount.clar b/contracts/dao/proposals/aibtc-timed-vault-sbtc-set-withdrawal-amount.clar new file mode 100644 index 00000000..db07e1d3 --- /dev/null +++ b/contracts/dao/proposals/aibtc-timed-vault-sbtc-set-withdrawal-amount.clar @@ -0,0 +1,15 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Set withdrawal amount in the BTC timed vault extension") +(define-constant CFG_WITHDRAWAL_AMOUNT u100000) ;; 0.001 BTC (8 decimals) + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; set the withdrawal amount + (contract-call? .aibtc-timed-vault-sbtc set-withdrawal-amount CFG_WITHDRAWAL_AMOUNT) + ) +) diff --git a/contracts/dao/proposals/aibtc-timed-vault-sbtc-set-withdrawal-period.clar b/contracts/dao/proposals/aibtc-timed-vault-sbtc-set-withdrawal-period.clar new file mode 100644 index 00000000..c44124d4 --- /dev/null +++ b/contracts/dao/proposals/aibtc-timed-vault-sbtc-set-withdrawal-period.clar @@ -0,0 +1,15 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Set withdrawal period in the BTC timed vault extension") +(define-constant CFG_WITHDRAWAL_PERIOD u144) + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; set the withdrawal period + (contract-call? .aibtc-timed-vault-sbtc set-withdrawal-period CFG_WITHDRAWAL_PERIOD) + ) +) diff --git a/contracts/dao/proposals/aibtc-timed-vault-sbtc-withdraw.clar b/contracts/dao/proposals/aibtc-timed-vault-sbtc-withdraw.clar new file mode 100644 index 00000000..f311821b --- /dev/null +++ b/contracts/dao/proposals/aibtc-timed-vault-sbtc-withdraw.clar @@ -0,0 +1,14 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Withdrew BTC from the timed vault extension") + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; withdraw sBTC from the timed vault + (contract-call? .aibtc-timed-vault-sbtc withdraw) + ) +) diff --git a/contracts/dao/proposals/aibtc-timed-vault-initialize-new-account.clar b/contracts/dao/proposals/aibtc-timed-vault-stx-initialize-new-vault.clar similarity index 58% rename from contracts/dao/proposals/aibtc-timed-vault-initialize-new-account.clar rename to contracts/dao/proposals/aibtc-timed-vault-stx-initialize-new-vault.clar index 58347054..8354ac6e 100644 --- a/contracts/dao/proposals/aibtc-timed-vault-initialize-new-account.clar +++ b/contracts/dao/proposals/aibtc-timed-vault-stx-initialize-new-vault.clar @@ -1,9 +1,9 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; -(define-constant CFG_MESSAGE "Executed Core Proposal: Initialized a new timed vault in the base dao and funded it from the treasury") -(define-constant CFG_ACCOUNT_HOLDER 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM) +(define-constant CFG_MESSAGE "Executed Core Proposal: Initialized a new STX timed vault in the base dao and funded it from the treasury") +(define-constant CFG_ACCOUNT_HOLDER 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5) (define-constant CFG_AMOUNT_TO_FUND_STX u100) ;; set to 0 to skip, in microSTX (define-constant CFG_AMOUNT_TO_FUND_FT u100) ;; set to 0 to skip, in microFT @@ -12,14 +12,14 @@ ;; send a message from the dao (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) ;; CFG_MESSAGE_CONTRACT ;; set the account holder in the timed vault - (try! (contract-call? .aibtc-timed-vault set-account-holder CFG_ACCOUNT_HOLDER)) ;; CFG_NEW_TIMED_VAULT_CONTRACT + (try! (contract-call? .aibtc-timed-vault-stx set-account-holder CFG_ACCOUNT_HOLDER)) ;; CFG_NEW_TIMED_VAULT_CONTRACT ;; enable the extension in the dao - (try! (contract-call? .aibtc-base-dao set-extension .aibtc-timed-vault true)) ;; CFG_BASE_DAO, CFG_NEW_TIMED_VAULT_CONTRACT + (try! (contract-call? .aibtc-base-dao set-extension .aibtc-timed-vault-stx true)) ;; CFG_BASE_DAO, CFG_NEW_TIMED_VAULT_CONTRACT ;; fund the extension from the treasury (and (> CFG_AMOUNT_TO_FUND_STX u0) - (try! (contract-call? .aibtc-treasury withdraw-stx CFG_AMOUNT_TO_FUND_STX .aibtc-timed-vault))) ;; CFG_TREASURY_CONTRACT, CFG_NEW_TIMED_VAULT_CONTRACT + (try! (contract-call? .aibtc-treasury withdraw-stx CFG_AMOUNT_TO_FUND_STX .aibtc-timed-vault-stx))) ;; CFG_TREASURY_CONTRACT, CFG_NEW_TIMED_VAULT_CONTRACT (and (> CFG_AMOUNT_TO_FUND_FT u0) - (try! (contract-call? .aibtc-treasury withdraw-ft .aibtc-token CFG_AMOUNT_TO_FUND_FT .aibtc-timed-vault))) ;; CFG_TREASURY_CONTRACT, CFG_TOKEN_CONTRACT, CFG_NEW_TIMED_VAULT_CONTRACT + (try! (contract-call? .aibtc-treasury withdraw-ft .aibtc-token CFG_AMOUNT_TO_FUND_FT .aibtc-timed-vault-stx))) ;; CFG_TREASURY_CONTRACT, CFG_TOKEN_CONTRACT, CFG_NEW_TIMED_VAULT_CONTRACT (ok true) ) ) diff --git a/contracts/dao/proposals/aibtc-timed-vault-override-last-withdrawal-block.clar b/contracts/dao/proposals/aibtc-timed-vault-stx-override-last-withdrawal-block.clar similarity index 67% rename from contracts/dao/proposals/aibtc-timed-vault-override-last-withdrawal-block.clar rename to contracts/dao/proposals/aibtc-timed-vault-stx-override-last-withdrawal-block.clar index 1c802cbb..e02a011a 100644 --- a/contracts/dao/proposals/aibtc-timed-vault-override-last-withdrawal-block.clar +++ b/contracts/dao/proposals/aibtc-timed-vault-stx-override-last-withdrawal-block.clar @@ -1,8 +1,8 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; -(define-constant CFG_MESSAGE "Executed Core Proposal: Overwrote last withdrawal block in the timed vault extension") +(define-constant CFG_MESSAGE "Executed Core Proposal: Overwrote last withdrawal block in the STX timed vault extension") ;; add 5 to avoid matching deployed height of contracts for testing (define-constant CFG_LAST_WITHDRAWAL_BLOCK (+ burn-block-height u5)) @@ -11,6 +11,6 @@ ;; send a message from the dao (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) ;; CFG_MESSAGE_CONTRACT ;; override last withdrawal block in the timed vault - (contract-call? .aibtc-timed-vault override-last-withdrawal-block CFG_LAST_WITHDRAWAL_BLOCK) ;; CFG_TIMED_VAULT_CONTRACT + (contract-call? .aibtc-timed-vault-stx override-last-withdrawal-block CFG_LAST_WITHDRAWAL_BLOCK) ;; CFG_TIMED_VAULT_CONTRACT ) ) diff --git a/contracts/dao/proposals/aibtc-timed-vault-set-account-holder.clar b/contracts/dao/proposals/aibtc-timed-vault-stx-set-account-holder.clar similarity index 51% rename from contracts/dao/proposals/aibtc-timed-vault-set-account-holder.clar rename to contracts/dao/proposals/aibtc-timed-vault-stx-set-account-holder.clar index 788ee451..954235e8 100644 --- a/contracts/dao/proposals/aibtc-timed-vault-set-account-holder.clar +++ b/contracts/dao/proposals/aibtc-timed-vault-stx-set-account-holder.clar @@ -1,15 +1,15 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; -(define-constant CFG_MESSAGE "Executed Core Proposal: Set or updated the account holder in the timed vault extension") -(define-constant CFG_ACCOUNT_HOLDER 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5) +(define-constant CFG_MESSAGE "Executed Core Proposal: Set or updated the account holder in the STX timed vault extension") +(define-constant CFG_ACCOUNT_HOLDER 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM) (define-public (execute (sender principal)) (begin ;; send a message from the dao (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) ;; set the account holder - (contract-call? .aibtc-timed-vault set-account-holder CFG_ACCOUNT_HOLDER) + (contract-call? .aibtc-timed-vault-stx set-account-holder CFG_ACCOUNT_HOLDER) ) ) diff --git a/contracts/dao/proposals/aibtc-timed-vault-set-withdrawal-amount.clar b/contracts/dao/proposals/aibtc-timed-vault-stx-set-withdrawal-amount.clar similarity index 53% rename from contracts/dao/proposals/aibtc-timed-vault-set-withdrawal-amount.clar rename to contracts/dao/proposals/aibtc-timed-vault-stx-set-withdrawal-amount.clar index ebb11288..fcb6a1db 100644 --- a/contracts/dao/proposals/aibtc-timed-vault-set-withdrawal-amount.clar +++ b/contracts/dao/proposals/aibtc-timed-vault-stx-set-withdrawal-amount.clar @@ -1,15 +1,15 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; -(define-constant CFG_MESSAGE "Executed Core Proposal: Set withdrawal amount in the timed vault extension") -(define-constant CFG_WITHDRAWAL_AMOUNT u10000000) +(define-constant CFG_MESSAGE "Executed Core Proposal: Set withdrawal amount in the STX timed vault extension") +(define-constant CFG_WITHDRAWAL_AMOUNT u10000000) ;; 10 STX (6 decimals) (define-public (execute (sender principal)) (begin ;; send a message from the dao (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) ;; set the withdrawal amount - (contract-call? .aibtc-timed-vault set-withdrawal-amount CFG_WITHDRAWAL_AMOUNT) + (contract-call? .aibtc-timed-vault-stx set-withdrawal-amount CFG_WITHDRAWAL_AMOUNT) ) ) diff --git a/contracts/dao/proposals/aibtc-timed-vault-set-withdrawal-period.clar b/contracts/dao/proposals/aibtc-timed-vault-stx-set-withdrawal-period.clar similarity index 62% rename from contracts/dao/proposals/aibtc-timed-vault-set-withdrawal-period.clar rename to contracts/dao/proposals/aibtc-timed-vault-stx-set-withdrawal-period.clar index 6892021b..49c09030 100644 --- a/contracts/dao/proposals/aibtc-timed-vault-set-withdrawal-period.clar +++ b/contracts/dao/proposals/aibtc-timed-vault-stx-set-withdrawal-period.clar @@ -1,17 +1,17 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; -(define-constant CFG_MESSAGE "Executed Core Proposal: Set withdrawal period in the timed vault extension") +(define-constant CFG_MESSAGE "Executed Core Proposal: Set withdrawal period in the STX timed vault extension") (define-constant CFG_WITHDRAWAL_PERIOD u144) ;; was CFG_MESSAGE_CONTRACT .aibtc-onchain-messaging -;; was CFG_TIMED_VAULT_EXTENSION .aibtc-timed-vault +;; was CFG_TIMED_VAULT_EXTENSION .aibtc-timed-vault-stx (define-public (execute (sender principal)) (begin ;; send a message from the dao (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) ;; set the withdrawal period - (contract-call? .aibtc-timed-vault set-withdrawal-period CFG_WITHDRAWAL_PERIOD) + (contract-call? .aibtc-timed-vault-stx set-withdrawal-period CFG_WITHDRAWAL_PERIOD) ) ) diff --git a/contracts/dao/proposals/aibtc-timed-vault-withdraw-stx.clar b/contracts/dao/proposals/aibtc-timed-vault-stx-withdraw.clar similarity index 77% rename from contracts/dao/proposals/aibtc-timed-vault-withdraw-stx.clar rename to contracts/dao/proposals/aibtc-timed-vault-stx-withdraw.clar index 5db08adf..aefecd30 100644 --- a/contracts/dao/proposals/aibtc-timed-vault-withdraw-stx.clar +++ b/contracts/dao/proposals/aibtc-timed-vault-stx-withdraw.clar @@ -1,4 +1,4 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; @@ -9,6 +9,6 @@ ;; send a message from the dao (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) ;; withdraw STX from the timed vault - (contract-call? .aibtc-timed-vault withdraw-stx) + (contract-call? .aibtc-timed-vault-stx withdraw) ) ) diff --git a/contracts/dao/proposals/aibtc-token-owner-set-token-uri.clar b/contracts/dao/proposals/aibtc-token-owner-set-token-uri.clar index a190dd7a..03117047 100644 --- a/contracts/dao/proposals/aibtc-token-owner-set-token-uri.clar +++ b/contracts/dao/proposals/aibtc-token-owner-set-token-uri.clar @@ -1,4 +1,4 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; diff --git a/contracts/dao/proposals/aibtc-token-owner-transfer-ownership.clar b/contracts/dao/proposals/aibtc-token-owner-transfer-ownership.clar index c685ed1b..7d0c6223 100644 --- a/contracts/dao/proposals/aibtc-token-owner-transfer-ownership.clar +++ b/contracts/dao/proposals/aibtc-token-owner-transfer-ownership.clar @@ -1,4 +1,4 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; diff --git a/contracts/dao/proposals/aibtc-treasury-allow-asset.clar b/contracts/dao/proposals/aibtc-treasury-allow-asset.clar index 23472b10..aa033a33 100644 --- a/contracts/dao/proposals/aibtc-treasury-allow-asset.clar +++ b/contracts/dao/proposals/aibtc-treasury-allow-asset.clar @@ -1,11 +1,9 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; -(define-constant CFG_MESSAGE "Executed Core Proposal: Allowed or enabled asset for use in the treasury extension") -(define-constant CFG_ASSET 'SP3D6PV2ACBPEKYJTCMH7HEN02KP87QSP8KTEH335.abtc) -;; was CFG_MESSAGE_CONTRACT .aibtc-onchain-messaging -;; was CFG_TREASURY_CONTRACT .aibtc-treasury +(define-constant CFG_MESSAGE "Executed Core Proposal: Allowed asset for use in the treasury extension") +(define-constant CFG_ASSET 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.fake-token-1) (define-public (execute (sender principal)) (begin diff --git a/contracts/dao/proposals/aibtc-treasury-allow-assets.clar b/contracts/dao/proposals/aibtc-treasury-allow-assets.clar new file mode 100644 index 00000000..0e5f9baa --- /dev/null +++ b/contracts/dao/proposals/aibtc-treasury-allow-assets.clar @@ -0,0 +1,22 @@ +(impl-trait .aibtc-dao-traits-v3.proposal) + +;; template vars +;; +(define-constant CFG_MESSAGE "Executed Core Proposal: Allowed multiple assets for use in the treasury extension") +(define-constant CFG_ASSET_LIST (list + {token: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.fake-token-1, enabled: true} + {token: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.fake-token-2, enabled: true} + {token: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.fake-token-3, enabled: true} + {token: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.fake-token-4, enabled: true} + {token: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.fake-token-5, enabled: true} +)) + +(define-public (execute (sender principal)) + (begin + ;; send a message from the dao + (try! (contract-call? .aibtc-onchain-messaging send CFG_MESSAGE true)) + ;; allow an asset for deposit and withdrawal in the treasury + (try! (contract-call? .aibtc-treasury allow-assets CFG_ASSET_LIST)) + (ok true) + ) +) diff --git a/contracts/dao/proposals/aibtc-treasury-delegate-stx.clar b/contracts/dao/proposals/aibtc-treasury-delegate-stx.clar index aadcbb3c..c7eba452 100644 --- a/contracts/dao/proposals/aibtc-treasury-delegate-stx.clar +++ b/contracts/dao/proposals/aibtc-treasury-delegate-stx.clar @@ -1,4 +1,4 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; diff --git a/contracts/dao/proposals/aibtc-treasury-disable-asset.clar b/contracts/dao/proposals/aibtc-treasury-disable-asset.clar index f81348db..2c920990 100644 --- a/contracts/dao/proposals/aibtc-treasury-disable-asset.clar +++ b/contracts/dao/proposals/aibtc-treasury-disable-asset.clar @@ -1,4 +1,4 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; diff --git a/contracts/dao/proposals/aibtc-treasury-revoke-delegation.clar b/contracts/dao/proposals/aibtc-treasury-revoke-delegation.clar index 1046ee42..015f3abc 100644 --- a/contracts/dao/proposals/aibtc-treasury-revoke-delegation.clar +++ b/contracts/dao/proposals/aibtc-treasury-revoke-delegation.clar @@ -1,4 +1,4 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; diff --git a/contracts/dao/proposals/aibtc-treasury-withdraw-ft.clar b/contracts/dao/proposals/aibtc-treasury-withdraw-ft.clar index f4649978..84fab03b 100644 --- a/contracts/dao/proposals/aibtc-treasury-withdraw-ft.clar +++ b/contracts/dao/proposals/aibtc-treasury-withdraw-ft.clar @@ -1,4 +1,4 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; diff --git a/contracts/dao/proposals/aibtc-treasury-withdraw-nft.clar b/contracts/dao/proposals/aibtc-treasury-withdraw-nft.clar index 4d30cd67..172d96dc 100644 --- a/contracts/dao/proposals/aibtc-treasury-withdraw-nft.clar +++ b/contracts/dao/proposals/aibtc-treasury-withdraw-nft.clar @@ -1,4 +1,4 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; diff --git a/contracts/dao/proposals/aibtc-treasury-withdraw-stx.clar b/contracts/dao/proposals/aibtc-treasury-withdraw-stx.clar index 535db60d..9dd45752 100644 --- a/contracts/dao/proposals/aibtc-treasury-withdraw-stx.clar +++ b/contracts/dao/proposals/aibtc-treasury-withdraw-stx.clar @@ -1,4 +1,4 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) ;; template vars ;; diff --git a/contracts/dao/token/aibtc-bitflow-pool.clar b/contracts/dao/token/aibtc-bitflow-pool.clar index 22298c37..1c012331 100644 --- a/contracts/dao/token/aibtc-bitflow-pool.clar +++ b/contracts/dao/token/aibtc-bitflow-pool.clar @@ -3,7 +3,7 @@ (use-trait sip-010-trait 'ST3VXT52QEQPZ5246A16RFNMR1PRJ96JK6YYX37N8.sip-010-trait-ft-standard.sip-010-trait) ;; <%= it.sip10_trait %> ;; implement aibtcdev trait -(impl-trait .aibtc-dao-traits-v2.bitflow-pool) +(impl-trait .aibtc-dao-traits-v3.bitflow-pool) ;; Define fungible pool token (define-fungible-token pool-token) diff --git a/contracts/dao/token/aibtc-token-dex.clar b/contracts/dao/token/aibtc-token-dex.clar index 8d966c0a..6ecb9d68 100644 --- a/contracts/dao/token/aibtc-token-dex.clar +++ b/contracts/dao/token/aibtc-token-dex.clar @@ -1,7 +1,7 @@ ;; 99af7ff63e5e4bd7542e55d88bacc25a7a6f79004f9937ea0bab3ca4c2438061 ;; aibtc.dev DAO faktory.fun DEX @version 1.0 - (impl-trait .aibtc-dao-traits-v2.faktory-dex) ;; <%= it.token_faktory_dex_trait %> + (impl-trait .aibtc-dao-traits-v3.faktory-dex) ;; <%= it.token_faktory_dex_trait %> (impl-trait .faktory-dex-trait-v1-1.dex-trait) ;; <%= it.faktory_dex_trait %> (use-trait faktory-token .faktory-trait-v1.sip-010-trait) ;; <%= it.faktory_sip10_trait %> diff --git a/contracts/dao/token/aibtc-token.clar b/contracts/dao/token/aibtc-token.clar index 1d50a6e9..b49624e9 100644 --- a/contracts/dao/token/aibtc-token.clar +++ b/contracts/dao/token/aibtc-token.clar @@ -2,7 +2,7 @@ ;; <%= it.token_symbol %> Powered By Faktory.fun v1.0 (impl-trait .faktory-trait-v1.sip-010-trait) ;; 'SP3XXMS38VTAWTVPE5682XSBFXPTH7XCPEBTX8AN2 -(impl-trait .aibtc-dao-traits-v2.token) ;; 'SP29CK9990DQGE9RGTT1VEQTTYH8KY4E3JE5XP4EC +(impl-trait .aibtc-dao-traits-v3.token) ;; 'SP29CK9990DQGE9RGTT1VEQTTYH8KY4E3JE5XP4EC (define-constant ERR-NOT-AUTHORIZED u401) (define-constant ERR-NOT-OWNER u402) diff --git a/contracts/dao/traits/aibtc-dao-traits-v3.clar b/contracts/dao/traits/aibtc-dao-traits-v3.clar new file mode 100644 index 00000000..e35b70c9 --- /dev/null +++ b/contracts/dao/traits/aibtc-dao-traits-v3.clar @@ -0,0 +1,252 @@ +;; title: aibtc-dao-traits +;; version: 3.0.0 +;; summary: A collection of traits for all aibtc daos. + +;; IMPORTS +(use-trait faktory-token .faktory-trait-v1.sip-010-trait) +(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 + +;; a one-time action proposed by token holders +(define-trait proposal ( + (execute (principal) (response bool uint)) +)) + +;; a standing feature of the dao implemented in Clarity +(define-trait extension ( + (callback (principal (buff 34)) (response bool uint)) +)) + +;; TOKEN TRAITS + +;; the decentralized Bitflow trading pool following their xyk formula +(define-trait bitflow-pool ( + ;; transfer funds (we're just tagging this contract) + ;; all functions are covered between sip-010 and bitflow-xyk + (transfer (uint principal principal (optional (buff 34))) (response bool uint)) +)) + +;; the decentralized exchange and initial bonding curve for a token +;; can be used to buy and sell tokens until the target is reached +;; liquidity is provided by the initial minting of tokens (20%) +;; reaching the target will trigger migration to the Bitflow pool +(define-trait faktory-dex ( + ;; buy tokens from the dex + ;; @param ft the token contract + ;; @param ustx the amount of microSTX to spend + ;; @returns (response bool uint) + (buy ( uint) (response bool uint)) + ;; sell tokens to the dex + ;; @param ft the token contract + ;; @param amount the amount of tokens to sell + ;; @returns (response bool uint) + (sell ( uint) (response bool uint)) +)) + +;; the token contract for the dao, with no pre-mine or initial allocation +(define-trait token ( + ;; transfer funds (limited as we're just tagging this) + (transfer (uint principal principal (optional (buff 34))) (response bool uint)) +)) + +;; EXTENSION TRAITS + +;; a pre-defined action that token holders can propose +(define-trait action ( + ;; @param parameters serialized hex-encoded Clarity values + ;; @returns (response bool uint) + (run ((buff 2048)) (response bool uint)) +)) + +;; a voting contract for whitelisted pre-defined actions +;; has lower voting threshold and quorum than core proposals +(define-trait action-proposals ( + ;; propose a new action + ;; @param action the action contract + ;; @param parameters encoded action parameters + ;; @returns (response bool uint) + (propose-action ( (buff 2048) (optional (string-ascii 1024))) (response bool uint)) + ;; vote on an existing proposal + ;; @param proposal the proposal id + ;; @param vote true for yes, false for no + ;; @returns (response bool uint) + (vote-on-proposal (uint bool) (response bool uint)) + ;; conclude a proposal after voting period + ;; @param proposal the proposal id + ;; @param action the action contract + ;; @returns (response bool uint) + (conclude-proposal (uint ) (response bool uint)) +)) + +;; a smart contract that can be funded and assigned to a principal +;; withdrawals are based on a set amount and time period in blocks +(define-trait timed-vault ( + ;; set account holder + ;; @param principal the new account holder who can withdraw + ;; @returns (response bool uint) + (set-account-holder (principal) (response bool uint)) + ;; set withdrawal period + ;; @param period the new withdrawal period in Bitcoin blocks + ;; @returns (response bool uint) + (set-withdrawal-period (uint) (response bool uint)) + ;; set withdrawal amount + ;; @param amount the new withdrawal amount in micro-units + ;; @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 funds to the timed vault + ;; @param amount amount of token to deposit in micro-units + ;; @returns (response bool uint) + (deposit (uint) (response bool uint)) + ;; withdraw funds from the timed vault + ;; @returns (response bool uint) + (withdraw () (response bool uint)) +)) + +;; an extension to manage the dao charter and mission +;; allows the dao to define its mission and values on-chain +;; used to guide decision-making and proposals +(define-trait charter ( + ;; set the dao charter + ;; @param charter the new charter text + ;; @returns (response bool uint) + (set-dao-charter ((string-ascii 4096) (optional (buff 33))) (response bool uint)) +)) + +;; a voting contract for core dao proposals +;; has higher voting threshold and quorum than action proposals +;; can run any Clarity code in the context of the dao +(define-trait core-proposals ( + ;; create a new proposal + ;; @param proposal the proposal contract + ;; @returns (response bool uint) + (create-proposal ( (optional (string-ascii 1024))) (response bool uint)) + ;; vote on an existing proposal + ;; @param proposal the proposal 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 + ;; @returns (response bool uint) + (conclude-proposal () (response bool uint)) +)) + +;; a messaging contract that allows anyone to send public messages on-chain +;; messages can be up to 1MB in size and are printed as events that can be monitored +;; messages can verifiably indicate the sender is the dao by using a proposal +(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)) +)) + +;; an invoicing contract that allows anyone to pay invoices +;; used in conjunction with the 'resources' trait +(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)) +)) + +;; a resource contract that allows for management of resources +;; resources can be paid for by anyone, and toggled on/off +;; used in conjunction with the 'invoices' trait for a payment system +(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)) +)) + +;; an extension that manages the token on behalf of the dao +;; allows for same functionality normally used by deployer through proposals +(define-trait token-owner ( + ;; set the token URI + ;; @param value the new token URI + ;; @returns (response bool uint) + (set-token-uri ((string-utf8 256)) (response bool uint)) + ;; transfer ownership of the token + ;; @param new-owner the new owner of the token + ;; @returns (response bool uint) + (transfer-ownership (principal) (response bool uint)) +)) + +;; an extension that manages STX, SIP-009 NFTs, and SIP-010 tokens +;; also supports stacking STX with Stacks Proof of Transfer +(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) + (allow-assets ((list 100 {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)) +)) diff --git a/contracts/dao/traits/aibtc-dao-v2.clar b/contracts/dao/traits/aibtc-dao-v2.clar index e273f5b0..ea1c38ce 100644 --- a/contracts/dao/traits/aibtc-dao-v2.clar +++ b/contracts/dao/traits/aibtc-dao-v2.clar @@ -1,5 +1,5 @@ -(use-trait proposal-trait .aibtc-dao-traits-v2.proposal) -(use-trait extension-trait .aibtc-dao-traits-v2.extension) +(use-trait proposal-trait .aibtc-dao-traits-v3.proposal) +(use-trait extension-trait .aibtc-dao-traits-v3.extension) (define-trait aibtc-base-dao ( ;; Execute a governance proposal diff --git a/contracts/dao/traits/aibtc-dao-v3.clar b/contracts/dao/traits/aibtc-dao-v3.clar new file mode 100644 index 00000000..7f35b875 --- /dev/null +++ b/contracts/dao/traits/aibtc-dao-v3.clar @@ -0,0 +1,15 @@ +;; title: aibtc-dao +;; version: 3.0.0 +;; summary: A trait that defines an aibtc base dao. + +(use-trait proposal-trait .aibtc-dao-traits-v3.proposal) +(use-trait extension-trait .aibtc-dao-traits-v3.extension) + +(define-trait aibtc-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 index 28913239..d62b8579 100644 --- a/contracts/test/aibtc-treasury.clar +++ b/contracts/test/aibtc-treasury.clar @@ -1,6 +1,6 @@ ;; test treasury contract implementing treasury trait (impl-trait .aibtcdev-dao-traits-v1.treasury) -(impl-trait .aibtc-dao-traits-v2.treasury) +(impl-trait .aibtc-dao-traits-v3.treasury) (use-trait ft-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) (use-trait nft-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait) diff --git a/contracts/test/disable-action-proposals-v2.clar b/contracts/test/disable-action-proposals-v2.clar index 02e4c9bf..c722cfeb 100644 --- a/contracts/test/disable-action-proposals-v2.clar +++ b/contracts/test/disable-action-proposals-v2.clar @@ -1,4 +1,4 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) (define-constant ERR_EXTENSION_NOT_FOUND (err u3003)) diff --git a/contracts/test/disable-core-proposals-v2.clar b/contracts/test/disable-core-proposals-v2.clar index 4eb12e4d..785e73e2 100644 --- a/contracts/test/disable-core-proposals-v2.clar +++ b/contracts/test/disable-core-proposals-v2.clar @@ -1,4 +1,4 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) (define-constant ERR_EXTENSION_NOT_FOUND (err u3003)) diff --git a/contracts/test/disable-onchain-messaging-action.clar b/contracts/test/disable-onchain-messaging-action.clar index 94c26a94..90a047e8 100644 --- a/contracts/test/disable-onchain-messaging-action.clar +++ b/contracts/test/disable-onchain-messaging-action.clar @@ -1,4 +1,4 @@ -(impl-trait .aibtc-dao-traits-v2.proposal) +(impl-trait .aibtc-dao-traits-v3.proposal) (define-constant ERR_EXTENSION_NOT_FOUND (err u3003)) diff --git a/tests/aibtc-user-agent-smart-wallet.test.ts b/tests/aibtc-user-agent-smart-wallet.test.ts index 4bed8f31..c7a1b0d5 100644 --- a/tests/aibtc-user-agent-smart-wallet.test.ts +++ b/tests/aibtc-user-agent-smart-wallet.test.ts @@ -582,6 +582,8 @@ describe(`public functions: ${contractName}`, () => { //////////////////////////////////////// // proxy-propose-action() tests //////////////////////////////////////// + const memoContext = "Can pass up to 1024 characters for additional context."; + it("proxy-propose-action() fails if caller is not authorized (user or agent)", () => { // arrange const message = Cl.stringAscii("hello world"); @@ -593,6 +595,7 @@ describe(`public functions: ${contractName}`, () => { Cl.principal(actionProposalsV2ContractAddress), Cl.principal(sendMessageActionContractAddress), Cl.buffer(Cl.serialize(message)), + Cl.some(Cl.stringAscii(memoContext)), ], address3 ); @@ -612,6 +615,7 @@ describe(`public functions: ${contractName}`, () => { Cl.principal(actionProposalsV2ContractAddress), Cl.principal(sendMessageActionContractAddress), Cl.buffer(Cl.serialize(message)), + Cl.some(Cl.stringAscii(memoContext)), ], deployer ); @@ -641,6 +645,7 @@ describe(`public functions: ${contractName}`, () => { Cl.principal(actionProposalsV2ContractAddress), Cl.principal(sendMessageActionContractAddress), Cl.buffer(Cl.serialize(message)), + Cl.some(Cl.stringAscii(memoContext)), ], deployer ); @@ -662,6 +667,7 @@ describe(`public functions: ${contractName}`, () => { [ Cl.principal(coreProposalsV2ContractAddress), Cl.principal(baseEnableExtensionContractAddress), + Cl.some(Cl.stringAscii(memoContext)), ], address3 ); @@ -681,6 +687,7 @@ describe(`public functions: ${contractName}`, () => { [ Cl.principal(coreProposalsV2ContractAddress), Cl.principal(baseEnableExtensionContractAddress), + Cl.some(Cl.stringAscii(memoContext)), ], deployer ); @@ -710,6 +717,7 @@ describe(`public functions: ${contractName}`, () => { [ Cl.principal(coreProposalsV2ContractAddress), Cl.principal(baseEnableExtensionContractAddress), + Cl.some(Cl.stringAscii(memoContext)), ], deployer ); @@ -756,6 +764,7 @@ describe(`public functions: ${contractName}`, () => { Cl.principal(actionProposalsV2ContractAddress), Cl.principal(sendMessageActionContractAddress), Cl.buffer(Cl.serialize(message)), + Cl.some(Cl.stringAscii(memoContext)), ], deployer ); @@ -791,6 +800,7 @@ describe(`public functions: ${contractName}`, () => { Cl.principal(actionProposalsV2ContractAddress), Cl.principal(sendMessageActionContractAddress), Cl.buffer(Cl.serialize(message)), + Cl.some(Cl.stringAscii(memoContext)), ], deployer ); @@ -860,6 +870,7 @@ describe(`public functions: ${contractName}`, () => { [ Cl.principal(coreProposalsV2ContractAddress), Cl.principal(baseEnableExtensionContractAddress), + Cl.some(Cl.stringAscii(memoContext)), ], deployer ); @@ -894,6 +905,7 @@ describe(`public functions: ${contractName}`, () => { [ Cl.principal(coreProposalsV2ContractAddress), Cl.principal(baseEnableExtensionContractAddress), + Cl.some(Cl.stringAscii(memoContext)), ], deployer ); @@ -968,6 +980,7 @@ describe(`public functions: ${contractName}`, () => { Cl.principal(actionProposalsV2ContractAddress), Cl.principal(sendMessageActionContractAddress), Cl.buffer(Cl.serialize(message)), + Cl.some(Cl.stringAscii(memoContext)), ], deployer ); @@ -1050,6 +1063,7 @@ describe(`public functions: ${contractName}`, () => { Cl.principal(actionProposalsV2ContractAddress), Cl.principal(sendMessageActionContractAddress), Cl.buffer(Cl.serialize(message)), + Cl.some(Cl.stringAscii(memoContext)), ], deployer ); @@ -1142,6 +1156,7 @@ describe(`public functions: ${contractName}`, () => { [ Cl.principal(coreProposalsV2ContractAddress), Cl.principal(baseEnableExtensionContractAddress), + Cl.some(Cl.stringAscii(memoContext)), ], deployer ); diff --git a/tests/check-test-coverage.sh b/tests/check-test-coverage.sh index 0fdedfde..9ddb14fe 100755 --- a/tests/check-test-coverage.sh +++ b/tests/check-test-coverage.sh @@ -44,8 +44,8 @@ echo -e "\nChecking test file coverage..." echo "===================================" for contract in "${contracts[@]}"; do # band-aid to skip trait file in diff location - if [[ "$contract" == *"aibtc-smart-wallet-traits.clar"* ]]; then - echo "⏩ Skipping excluded file: $contract" + if [[ "$contract" == *"traits"* ]]; then + echo "⏩ Skipping trait file: $contract" continue fi diff --git a/tests/dao-types.ts b/tests/dao-types.ts index 915c56c4..90cdf2a2 100644 --- a/tests/dao-types.ts +++ b/tests/dao-types.ts @@ -17,36 +17,56 @@ export enum ContractType { // dao extensions DAO_ACTION_PROPOSALS = "aibtc-action-proposals", DAO_ACTION_PROPOSALS_V2 = "aibtc-action-proposals-v2", - DAO_TIMED_VAULT = "aibtc-timed-vault", + DAO_TIMED_VAULT_DAO = "aibtc-timed-vault-dao", + DAO_TIMED_VAULT_SBTC = "aibtc-timed-vault-sbtc", + DAO_TIMED_VAULT_STX = "aibtc-timed-vault-stx", DAO_CHARTER = "aibtc-dao-charter", DAO_CORE_PROPOSALS = "aibtc-core-proposals", DAO_CORE_PROPOSALS_V2 = "aibtc-core-proposals-v2", DAO_MESSAGING = "aibtc-onchain-messaging", - DAO_PAYMENTS = "aibtc-payments-invoices", + DAO_PAYMENT_PROCESSOR_DAO = "aibtc-payment-processor-dao", + DAO_PAYMENT_PROCESSOR_SBTC = "aibtc-payment-processor-sbtc", + DAO_PAYMENT_PROCESSOR_STX = "aibtc-payment-processor-stx", DAO_TOKEN_OWNER = "aibtc-token-owner", DAO_TREASURY = "aibtc-treasury", } export enum ContractActionType { // dao extension actions - DAO_ACTION_ADD_RESOURCE = "aibtc-action-add-resource", - DAO_ACTION_ALLOW_ASSET = "aibtc-action-allow-asset", + DAO_ACTION_ALLOW_ASSET = "aibtc-action-treasury-allow-asset", + DAO_ACTION_CONFIGURE_TIMED_VAULT_DAO = "aibtc-action-configure-timed-vault-dao", + DAO_ACTION_CONFIGURE_TIMED_VAULT_SBTC = "aibtc-action-configure-timed-vault-sbtc", + DAO_ACTION_CONFIGURE_TIMED_VAULT_STX = "aibtc-action-configure-timed-vault-stx", + DAO_ACTION_PMT_DAO_ADD_RESOURCE = "aibtc-action-pmt-dao-add-resource", + DAO_ACTION_PMT_DAO_TOGGLE_RESOURCE = "aibtc-action-pmt-dao-toggle-resource", + DAO_ACTION_PMT_SBTC_ADD_RESOURCE = "aibtc-action-pmt-sbtc-add-resource", + DAO_ACTION_PMT_SBTC_TOGGLE_RESOURCE = "aibtc-action-pmt-sbtc-toggle-resource", + DAO_ACTION_PMT_STX_ADD_RESOURCE = "aibtc-action-pmt-stx-add-resource", + DAO_ACTION_PMT_STX_TOGGLE_RESOURCE = "aibtc-action-pmt-stx-toggle-resource", DAO_ACTION_SEND_MESSAGE = "aibtc-action-send-message", - DAO_ACTION_SET_ACCOUNT_HOLDER = "aibtc-action-set-account-holder", - DAO_ACTION_SET_WITHDRAWAL_AMOUNT = "aibtc-action-set-withdrawal-amount", - DAO_ACTION_SET_WITHDRAWAL_PERIOD = "aibtc-action-set-withdrawal-period", - DAO_ACTION_TOGGLE_RESOURCE_BY_NAME = "aibtc-action-toggle-resource-by-name", } export enum ContractProposalType { // dao proposal templates DAO_ACTION_PROPOSALS_SET_PROPOSAL_BOND = "aibtc-action-proposals-set-proposal-bond", - DAO_TIMED_VAULT_INITIALIZE_NEW_ACCOUNT = "aibtc-timed-vault-initialize-new-account", - DAO_TIMED_VAULT_OVERRIDE_LAST_WITHDRAWAL_BLOCK = "aibtc-timed-vault-override-last-withdrawal-block", - DAO_TIMED_VAULT_SET_ACCOUNT_HOLDER = "aibtc-timed-vault-set-account-holder", - DAO_TIMED_VAULT_SET_WITHDRAWAL_AMOUNT = "aibtc-timed-vault-set-withdrawal-amount", - DAO_TIMED_VAULT_SET_WITHDRAWAL_PERIOD = "aibtc-timed-vault-set-withdrawal-period", - DAO_TIMED_VAULT_WITHDRAW_STX = "aibtc-timed-vault-withdraw-stx", + DAO_TIMED_VAULT_DAO_INITIALIZE_NEW_ACCOUNT = "aibtc-timed-vault-dao-initialize-new-vault", + DAO_TIMED_VAULT_DAO_OVERRIDE_LAST_WITHDRAWAL_BLOCK = "aibtc-timed-vault-dao-override-last-withdrawal-block", + DAO_TIMED_VAULT_DAO_SET_ACCOUNT_HOLDER = "aibtc-timed-vault-dao-set-account-holder", + DAO_TIMED_VAULT_DAO_SET_WITHDRAWAL_AMOUNT = "aibtc-timed-vault-dao-set-withdrawal-amount", + DAO_TIMED_VAULT_DAO_SET_WITHDRAWAL_PERIOD = "aibtc-timed-vault-dao-set-withdrawal-period", + DAO_TIMED_VAULT_DAO_WITHDRAW = "aibtc-timed-vault-dao-withdraw", + DAO_TIMED_VAULT_SBTC_INITIALIZE_NEW_ACCOUNT = "aibtc-timed-vault-sbtc-initialize-new-vault", + DAO_TIMED_VAULT_SBTC_OVERRIDE_LAST_WITHDRAWAL_BLOCK = "aibtc-timed-vault-sbtc-override-last-withdrawal-block", + DAO_TIMED_VAULT_SBTC_SET_ACCOUNT_HOLDER = "aibtc-timed-vault-sbtc-set-account-holder", + DAO_TIMED_VAULT_SBTC_SET_WITHDRAWAL_AMOUNT = "aibtc-timed-vault-sbtc-set-withdrawal-amount", + DAO_TIMED_VAULT_SBTC_SET_WITHDRAWAL_PERIOD = "aibtc-timed-vault-sbtc-set-withdrawal-period", + DAO_TIMED_VAULT_SBTC_WITHDRAW = "aibtc-timed-vault-sbtc-withdraw", + DAO_TIMED_VAULT_STX_INITIALIZE_NEW_ACCOUNT = "aibtc-timed-vault-stx-initialize-new-vault", + DAO_TIMED_VAULT_STX_OVERRIDE_LAST_WITHDRAWAL_BLOCK = "aibtc-timed-vault-stx-override-last-withdrawal-block", + DAO_TIMED_VAULT_STX_SET_ACCOUNT_HOLDER = "aibtc-timed-vault-stx-set-account-holder", + DAO_TIMED_VAULT_STX_SET_WITHDRAWAL_AMOUNT = "aibtc-timed-vault-stx-set-withdrawal-amount", + DAO_TIMED_VAULT_STX_SET_WITHDRAWAL_PERIOD = "aibtc-timed-vault-stx-set-withdrawal-period", + DAO_TIMED_VAULT_STX_WITHDRAW = "aibtc-timed-vault-stx-withdraw", DAO_BASE_ADD_NEW_EXTENSION = "aibtc-base-add-new-extension", DAO_BASE_BOOTSTRAP_INITIALIZATION = "aibtc-base-bootstrap-initialization", DAO_BASE_BOOTSTRAP_INITIALIZATION_V2 = "aibtc-base-bootstrap-initialization-v2", @@ -56,13 +76,22 @@ export enum ContractProposalType { DAO_BASE_REPLACE_EXTENSION_PROPOSAL_VOTING = "aibtc-base-replace-extension-proposal-voting", DAO_CORE_PROPOSALS_SET_PROPOSAL_BOND = "aibtc-core-proposals-set-proposal-bond", DAO_ONCHAIN_MESSAGING_SEND = "aibtc-onchain-messaging-send", - DAO_PAYMENTS_INVOICES_ADD_RESOURCE = "aibtc-payments-invoices-add-resource", - DAO_PAYMENTS_INVOICES_SET_PAYMENT_ADDRESS = "aibtc-payments-invoices-set-payment-address", - DAO_PAYMENTS_INVOICES_TOGGLE_RESOURCE_BY_NAME = "aibtc-payments-invoices-toggle-resource-by-name", - DAO_PAYMENTS_INVOICES_TOGGLE_RESOURCE = "aibtc-payments-invoices-toggle-resource", + DAO_PMT_DAO_ADD_RESOURCE = "aibtc-pmt-dao-add-resource", + DAO_PMT_DAO_SET_PAYMENT_ADDRESS = "aibtc-pmt-dao-set-payment-address", + DAO_PMT_DAO_TOGGLE_RESOURCE_BY_NAME = "aibtc-pmt-dao-toggle-resource-by-name", + DAO_PMT_DAO_TOGGLE_RESOURCE = "aibtc-pmt-dao-toggle-resource", + DAO_PMT_SBTC_ADD_RESOURCE = "aibtc-pmt-sbtc-add-resource", + DAO_PMT_SBTC_SET_PAYMENT_ADDRESS = "aibtc-pmt-sbtc-set-payment-address", + DAO_PMT_SBTC_TOGGLE_RESOURCE_BY_NAME = "aibtc-pmt-sbtc-toggle-resource-by-name", + DAO_PMT_SBTC_TOGGLE_RESOURCE = "aibtc-pmt-sbtc-toggle-resource", + DAO_PMT_STX_ADD_RESOURCE = "aibtc-pmt-stx-add-resource", + DAO_PMT_STX_SET_PAYMENT_ADDRESS = "aibtc-pmt-stx-set-payment-address", + DAO_PMT_STX_TOGGLE_RESOURCE_BY_NAME = "aibtc-pmt-stx-toggle-resource-by-name", + DAO_PMT_STX_TOGGLE_RESOURCE = "aibtc-pmt-stx-toggle-resource", DAO_TOKEN_OWNER_SET_TOKEN_URI = "aibtc-token-owner-set-token-uri", DAO_TOKEN_OWNER_TRANSFER_OWNERSHIP = "aibtc-token-owner-transfer-ownership", DAO_TREASURY_ALLOW_ASSET = "aibtc-treasury-allow-asset", + DAO_TREASURY_ALLOW_ASSETS = "aibtc-treasury-allow-assets", DAO_TREASURY_DELEGATE_STX = "aibtc-treasury-delegate-stx", DAO_TREASURY_DISABLE_ASSET = "aibtc-treasury-disable-asset", DAO_TREASURY_REVOKE_DELEGATION = "aibtc-treasury-revoke-delegation", @@ -86,8 +115,10 @@ export enum TraitType { DAO_BASE = "aibtcdev-dao-v1", DAO_TRAITS = "aibtcdev-dao-traits-v1", DAO_TRAITS_V1_1 = "aibtcdev-dao-traits-v1-1", - DAO_BASE_V2 = "aibtcdev-dao-v2", - DAO_TRAITS_V2 = "aibtcdev-dao-traits-v2", + DAO_BASE_V2 = "aibtc-dao-v2", + DAO_TRAITS_V2 = "aibtc-dao-traits-v2", + DAO_BASE_V3 = "aibtc-dao-v3", + DAO_TRAITS_V3 = "aibtc-dao-traits-v3", FAKTORY_TRAIT_V1 = "faktory-dex-trait-v1-1", SIP09 = "nft-trait", SIP10 = "sip-010-trait-ft-standard", diff --git a/tests/dao/extensions/actions/aibtc-action-configure-timed-vault-dao.test.ts b/tests/dao/extensions/actions/aibtc-action-configure-timed-vault-dao.test.ts new file mode 100644 index 00000000..5393a14a --- /dev/null +++ b/tests/dao/extensions/actions/aibtc-action-configure-timed-vault-dao.test.ts @@ -0,0 +1,710 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { + ContractActionType, + ContractProposalType, + ContractType, +} from "../../../dao-types"; +import { ActionErrCode } from "../../../error-codes"; +import { + constructDao, + dbgLog, + fundVoters, + passActionProposal, + VOTING_CONFIG, +} from "../../../test-utilities"; + +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 contractAddress = `${deployer}.${ContractActionType.DAO_ACTION_CONFIGURE_TIMED_VAULT_DAO}`; + +describe(`action extension: ${ContractActionType.DAO_ACTION_CONFIGURE_TIMED_VAULT_DAO}`, () => { + it("callback() should respond with (ok true)", () => { + const callback = simnet.callPublicFn( + contractAddress, + "callback", + [Cl.principal(deployer), Cl.bufferFromAscii("test")], + deployer + ); + expect(callback.result).toBeOk(Cl.bool(true)); + }); + + it("run() fails if called directly", () => { + const accountHolder = Cl.some(Cl.principal(address3)); + const withdrawalAmount = Cl.some(Cl.uint(1000)); + const withdrawalPeriod = Cl.some(Cl.uint(100)); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + const receipt = simnet.callPublicFn( + contractAddress, + "run", + [Cl.buffer(Cl.serialize(paramsCV))], + deployer + ); + expect(receipt.result).toBeErr(Cl.uint(ActionErrCode.ERR_UNAUTHORIZED)); + }); + + it("run() fails if called as a DAO action proposal with all three opt params as none", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + // false indicates proposal did not run + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(false)); + }); + + it("run() succeeds if called as a DAO action proposal", () => { + const accountHolder = Cl.some(Cl.principal(address3)); + const withdrawalAmount = Cl.some(Cl.uint(100000000)); + const withdrawalPeriod = Cl.some(Cl.uint(100)); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + dbgLog( + `concludeProposalReceipt: ${JSON.stringify(concludeProposalReceipt)}`, + { forceLog: true } + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() succeeds with only accountHolder parameter", () => { + const accountHolder = Cl.some(Cl.principal(address3)); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() succeeds with only withdrawal amount parameter", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.some(Cl.uint(200000000)); + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() succeeds with only withdrawal period parameter", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.some(Cl.uint(200)); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() fails with invalid withdrawal amount (zero)", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.some(Cl.uint(0)); // Invalid: amount = 0 + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + // Expect false as the action should not execute with invalid parameters + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(false)); + }); + + it("run() fails with invalid withdrawal amount (too large)", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.some(Cl.uint(1000000000001)); // Invalid: amount > 100000000 + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + // Expect false as the action should not execute with invalid parameters + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(false)); + }); + + it("run() fails with invalid withdrawal period (too small)", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.some(Cl.uint(5)); // Invalid: period < 6 + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + // Expect false as the action should not execute with invalid parameters + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(false)); + }); + + it("run() fails with invalid withdrawal period (too large)", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.some(Cl.uint(8065)); // Invalid: period > 8064 + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + // Expect false as the action should not execute with invalid parameters + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(false)); + }); + + it("run() succeeds with minimum valid withdrawal amount", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.some(Cl.uint(100000000)); // Minimum valid amount + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() succeeds with maximum valid withdrawal amount", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.some(Cl.uint(999999999999)); // Maximum valid amount + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() succeeds with minimum valid withdrawal period", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.some(Cl.uint(7)); // Minimum valid period + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() succeeds with maximum valid withdrawal period", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.some(Cl.uint(8063)); // Maximum valid period + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); +}); diff --git a/tests/dao/extensions/actions/aibtc-action-configure-timed-vault-sbtc.test.ts b/tests/dao/extensions/actions/aibtc-action-configure-timed-vault-sbtc.test.ts new file mode 100644 index 00000000..7b314e1a --- /dev/null +++ b/tests/dao/extensions/actions/aibtc-action-configure-timed-vault-sbtc.test.ts @@ -0,0 +1,705 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { + ContractActionType, + ContractProposalType, + ContractType, +} from "../../../dao-types"; +import { ActionErrCode } from "../../../error-codes"; +import { + constructDao, + fundVoters, + passActionProposal, + VOTING_CONFIG, +} from "../../../test-utilities"; + +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 contractAddress = `${deployer}.${ContractActionType.DAO_ACTION_CONFIGURE_TIMED_VAULT_SBTC}`; + +describe(`action extension: ${ContractActionType.DAO_ACTION_CONFIGURE_TIMED_VAULT_SBTC}`, () => { + it("callback() should respond with (ok true)", () => { + const callback = simnet.callPublicFn( + contractAddress, + "callback", + [Cl.principal(deployer), Cl.bufferFromAscii("test")], + deployer + ); + expect(callback.result).toBeOk(Cl.bool(true)); + }); + + it("run() fails if called directly", () => { + const accountHolder = Cl.some(Cl.principal(address3)); + const withdrawalAmount = Cl.some(Cl.uint(1000)); + const withdrawalPeriod = Cl.some(Cl.uint(100)); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + const receipt = simnet.callPublicFn( + contractAddress, + "run", + [Cl.buffer(Cl.serialize(paramsCV))], + deployer + ); + expect(receipt.result).toBeErr(Cl.uint(ActionErrCode.ERR_UNAUTHORIZED)); + }); + + it("run() fails if called as a DAO action proposal with all three opt params as none", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + // false indicates proposal did not run + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(false)); + }); + + it("run() succeeds if called as a DAO action proposal", () => { + const accountHolder = Cl.some(Cl.principal(address3)); + const withdrawalAmount = Cl.some(Cl.uint(1000)); + const withdrawalPeriod = Cl.some(Cl.uint(100)); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() succeeds with only accountHolder parameter", () => { + const accountHolder = Cl.some(Cl.principal(address3)); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() succeeds with only withdrawal amount parameter", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.some(Cl.uint(5000)); + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() succeeds with only withdrawal period parameter", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.some(Cl.uint(200)); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() fails with invalid withdrawal amount (zero)", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.some(Cl.uint(0)); // Invalid: amount = 0 + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + // Expect false as the action should not execute with invalid parameters + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(false)); + }); + + it("run() fails with invalid withdrawal amount (too large)", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.some(Cl.uint(100000001)); // Invalid: amount > 100000000 + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + // Expect false as the action should not execute with invalid parameters + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(false)); + }); + + it("run() fails with invalid withdrawal period (too small)", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.some(Cl.uint(5)); // Invalid: period < 6 + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + // Expect false as the action should not execute with invalid parameters + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(false)); + }); + + it("run() fails with invalid withdrawal period (too large)", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.some(Cl.uint(8065)); // Invalid: period > 8064 + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + // Expect false as the action should not execute with invalid parameters + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(false)); + }); + + it("run() succeeds with minimum valid withdrawal amount", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.some(Cl.uint(1000)); // Minimum valid amount + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() succeeds with maximum valid withdrawal amount", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.some(Cl.uint(9999999)); // Maximum valid amount + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() succeeds with minimum valid withdrawal period", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.some(Cl.uint(7)); // Minimum valid period + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() succeeds with maximum valid withdrawal period", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.some(Cl.uint(8063)); // Maximum valid period + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); +}); diff --git a/tests/dao/extensions/actions/aibtc-action-configure-timed-vault-stx.test.ts b/tests/dao/extensions/actions/aibtc-action-configure-timed-vault-stx.test.ts new file mode 100644 index 00000000..e9237471 --- /dev/null +++ b/tests/dao/extensions/actions/aibtc-action-configure-timed-vault-stx.test.ts @@ -0,0 +1,704 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { + ContractActionType, + ContractProposalType, + ContractType, +} from "../../../dao-types"; +import { ActionErrCode } from "../../../error-codes"; +import { + constructDao, + fundVoters, + passActionProposal, + VOTING_CONFIG, +} from "../../../test-utilities"; + +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 contractAddress = `${deployer}.${ContractActionType.DAO_ACTION_CONFIGURE_TIMED_VAULT_STX}`; + +describe(`action extension: ${ContractActionType.DAO_ACTION_CONFIGURE_TIMED_VAULT_STX}`, () => { + it("callback() should respond with (ok true)", () => { + const callback = simnet.callPublicFn( + contractAddress, + "callback", + [Cl.principal(deployer), Cl.bufferFromAscii("test")], + deployer + ); + expect(callback.result).toBeOk(Cl.bool(true)); + }); + + it("run() fails if called directly", () => { + const accountHolder = Cl.some(Cl.principal(address3)); + const withdrawalAmount = Cl.some(Cl.uint(1000)); + const withdrawalPeriod = Cl.some(Cl.uint(100)); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + const receipt = simnet.callPublicFn( + contractAddress, + "run", + [Cl.buffer(Cl.serialize(paramsCV))], + deployer + ); + expect(receipt.result).toBeErr(Cl.uint(ActionErrCode.ERR_UNAUTHORIZED)); + }); + + it("run() fails if called as a DAO action proposal with all three opt params as none", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + // false indicates proposal did not run + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(false)); + }); + + it("run() succeeds if called as a DAO action proposal", () => { + const accountHolder = Cl.some(Cl.principal(address3)); + const withdrawalAmount = Cl.some(Cl.uint(2000000)); + const withdrawalPeriod = Cl.some(Cl.uint(100)); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() succeeds with only accountHolder parameter", () => { + const accountHolder = Cl.some(Cl.principal(address3)); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() succeeds with only withdrawal amount parameter", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.some(Cl.uint(5000000)); + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() succeeds with only withdrawal period parameter", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.some(Cl.uint(200)); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() fails with invalid withdrawal amount (zero)", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.some(Cl.uint(0)); // Invalid: amount = 0 + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + // Expect false as the action should not execute with invalid parameters + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(false)); + }); + + it("run() fails with invalid withdrawal amount (too large)", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.some(Cl.uint(100000001)); // Invalid: amount > 100000000 + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + // Expect false as the action should not execute with invalid parameters + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(false)); + }); + + it("run() fails with invalid withdrawal period (too small)", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.some(Cl.uint(5)); // Invalid: period < 6 + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + // Expect false as the action should not execute with invalid parameters + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(false)); + }); + + it("run() fails with invalid withdrawal period (too large)", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.some(Cl.uint(8065)); // Invalid: period > 8064 + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + // Expect false as the action should not execute with invalid parameters + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(false)); + }); + it("run() succeeds with minimum valid withdrawal amount", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.some(Cl.uint(1000000)); // Minimum valid amount + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() succeeds with maximum valid withdrawal amount", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.some(Cl.uint(99999999)); // Maximum valid amount + const withdrawalPeriod = Cl.none(); + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() succeeds with minimum valid withdrawal period", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.some(Cl.uint(7)); // Minimum valid period + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); + + it("run() succeeds with maximum valid withdrawal period", () => { + const accountHolder = Cl.none(); + const withdrawalAmount = Cl.none(); + const withdrawalPeriod = Cl.some(Cl.uint(8063)); // Maximum valid period + const paramsCV = Cl.tuple({ + accountHolder, + amount: withdrawalAmount, + period: withdrawalPeriod, + }); + + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + paramsCV, + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); +}); diff --git a/tests/dao/extensions/actions/aibtc-action-add-resource.test.ts b/tests/dao/extensions/actions/aibtc-action-pmt-dao-add-resource.test.ts similarity index 96% rename from tests/dao/extensions/actions/aibtc-action-add-resource.test.ts rename to tests/dao/extensions/actions/aibtc-action-pmt-dao-add-resource.test.ts index 45cf1d4c..b38f09f5 100644 --- a/tests/dao/extensions/actions/aibtc-action-add-resource.test.ts +++ b/tests/dao/extensions/actions/aibtc-action-pmt-dao-add-resource.test.ts @@ -18,9 +18,9 @@ const deployer = accounts.get("deployer")!; const address1 = accounts.get("wallet_1")!; const address2 = accounts.get("wallet_2")!; -const contractAddress = `${deployer}.${ContractActionType.DAO_ACTION_ADD_RESOURCE}`; +const contractAddress = `${deployer}.${ContractActionType.DAO_ACTION_PMT_DAO_ADD_RESOURCE}`; -describe(`action extension: ${ContractActionType.DAO_ACTION_ADD_RESOURCE}`, () => { +describe(`action extension: ${ContractActionType.DAO_ACTION_PMT_DAO_ADD_RESOURCE}`, () => { it("callback() should respond with (ok true)", () => { const callback = simnet.callPublicFn( contractAddress, diff --git a/tests/dao/extensions/actions/aibtc-action-toggle-resource-by-name.test.ts b/tests/dao/extensions/actions/aibtc-action-pmt-dao-toggle-resource.test.ts similarity index 93% rename from tests/dao/extensions/actions/aibtc-action-toggle-resource-by-name.test.ts rename to tests/dao/extensions/actions/aibtc-action-pmt-dao-toggle-resource.test.ts index 40c6ca81..de7a7222 100644 --- a/tests/dao/extensions/actions/aibtc-action-toggle-resource-by-name.test.ts +++ b/tests/dao/extensions/actions/aibtc-action-pmt-dao-toggle-resource.test.ts @@ -1,4 +1,4 @@ -import { Cl, cvToJSON } from "@stacks/transactions"; +import { Cl } from "@stacks/transactions"; import { describe, expect, it } from "vitest"; import { ContractActionType, @@ -18,9 +18,9 @@ const deployer = accounts.get("deployer")!; const address1 = accounts.get("wallet_1")!; const address2 = accounts.get("wallet_2")!; -const contractAddress = `${deployer}.${ContractActionType.DAO_ACTION_TOGGLE_RESOURCE_BY_NAME}`; +const contractAddress = `${deployer}.${ContractActionType.DAO_ACTION_PMT_DAO_TOGGLE_RESOURCE}`; -describe(`action extension: ${ContractActionType.DAO_ACTION_TOGGLE_RESOURCE_BY_NAME}`, () => { +describe(`action extension: ${ContractActionType.DAO_ACTION_PMT_DAO_TOGGLE_RESOURCE}`, () => { it("callback() should respond with (ok true)", () => { const callback = simnet.callPublicFn( contractAddress, diff --git a/tests/dao/extensions/actions/aibtc-action-pmt-sbtc-add-resource.test.ts b/tests/dao/extensions/actions/aibtc-action-pmt-sbtc-add-resource.test.ts new file mode 100644 index 00000000..850c2e3e --- /dev/null +++ b/tests/dao/extensions/actions/aibtc-action-pmt-sbtc-add-resource.test.ts @@ -0,0 +1,96 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { + ContractActionType, + ContractProposalType, + ContractType, +} from "../../../dao-types"; +import { ActionErrCode } from "../../../error-codes"; +import { + constructDao, + fundVoters, + passActionProposal, + VOTING_CONFIG, +} from "../../../test-utilities"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const address1 = accounts.get("wallet_1")!; +const address2 = accounts.get("wallet_2")!; + +const contractAddress = `${deployer}.${ContractActionType.DAO_ACTION_PMT_SBTC_ADD_RESOURCE}`; + +describe(`action extension: ${ContractActionType.DAO_ACTION_PMT_SBTC_ADD_RESOURCE}`, () => { + it("callback() should respond with (ok true)", () => { + const callback = simnet.callPublicFn( + contractAddress, + "callback", + [Cl.principal(deployer), Cl.bufferFromAscii("test")], + deployer + ); + expect(callback.result).toBeOk(Cl.bool(true)); + }); + + it("run() fails if called directly", () => { + const resourceInfo = { + name: Cl.stringUtf8("test"), + description: Cl.stringUtf8("test description"), + price: Cl.uint(1), + url: Cl.some(Cl.stringUtf8("https://aibtc.dev")), + }; + const receipt = simnet.callPublicFn( + contractAddress, + "run", + [Cl.buffer(Cl.serialize(Cl.tuple(resourceInfo)))], + deployer + ); + expect(receipt.result).toBeErr(Cl.uint(ActionErrCode.ERR_UNAUTHORIZED)); + }); + + it("run() succeeds if called as a DAO action proposal", () => { + const resourceInfo = { + name: Cl.stringUtf8("test"), + description: Cl.stringUtf8("test description"), + price: Cl.uint(1), + url: Cl.some(Cl.stringUtf8("https://aibtc.dev")), + }; + // setup contract names + const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; + const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; + const actionProposalsContractAddress = `${deployer}.${ContractType.DAO_ACTION_PROPOSALS_V2}`; + const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; + + // setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_ACTION_PROPOSALS_V2]; + + // fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // pass action proposal + const concludeProposalReceipt = passActionProposal( + actionProposalsContractAddress, + contractAddress, + 1, // proposal ID + Cl.tuple(resourceInfo), + deployer, + deployer, + [deployer, address1, address2], + votingConfig + ); + + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + }); +}); diff --git a/tests/dao/extensions/actions/aibtc-action-set-account-holder.test.ts b/tests/dao/extensions/actions/aibtc-action-pmt-sbtc-toggle-resource.test.ts similarity index 82% rename from tests/dao/extensions/actions/aibtc-action-set-account-holder.test.ts rename to tests/dao/extensions/actions/aibtc-action-pmt-sbtc-toggle-resource.test.ts index db59626a..f788fdb2 100644 --- a/tests/dao/extensions/actions/aibtc-action-set-account-holder.test.ts +++ b/tests/dao/extensions/actions/aibtc-action-pmt-sbtc-toggle-resource.test.ts @@ -17,11 +17,10 @@ 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 contractAddress = `${deployer}.${ContractActionType.DAO_ACTION_SET_ACCOUNT_HOLDER}`; +const contractAddress = `${deployer}.${ContractActionType.DAO_ACTION_PMT_SBTC_TOGGLE_RESOURCE}`; -describe(`action extension: ${ContractActionType.DAO_ACTION_SET_ACCOUNT_HOLDER}`, () => { +describe(`action extension: ${ContractActionType.DAO_ACTION_PMT_SBTC_TOGGLE_RESOURCE}`, () => { it("callback() should respond with (ok true)", () => { const callback = simnet.callPublicFn( contractAddress, @@ -33,18 +32,18 @@ describe(`action extension: ${ContractActionType.DAO_ACTION_SET_ACCOUNT_HOLDER}` }); it("run() fails if called directly", () => { - const holderAddress = Cl.principal(address3); + const resourceName = Cl.stringUtf8("test"); const receipt = simnet.callPublicFn( contractAddress, "run", - [Cl.buffer(Cl.serialize(holderAddress))], + [Cl.buffer(Cl.serialize(resourceName))], deployer ); expect(receipt.result).toBeErr(Cl.uint(ActionErrCode.ERR_UNAUTHORIZED)); }); it("run() succeeds if called as a DAO action proposal", () => { - const holderAddress = Cl.principal(address3); + const resourceName = Cl.stringUtf8("test"); // setup contract names const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; @@ -75,13 +74,14 @@ describe(`action extension: ${ContractActionType.DAO_ACTION_SET_ACCOUNT_HOLDER}` actionProposalsContractAddress, contractAddress, 1, // proposal ID - holderAddress, + resourceName, deployer, deployer, [deployer, address1, address2], votingConfig ); - - expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + // result is false because action could not run in this state + // err ERR_RESOURCE_NOT_FOUND u5005 is thrown because resourceName does not exist + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(false)); }); }); diff --git a/tests/dao/extensions/actions/aibtc-action-set-withdrawal-amount.test.ts b/tests/dao/extensions/actions/aibtc-action-pmt-stx-add-resource.test.ts similarity index 80% rename from tests/dao/extensions/actions/aibtc-action-set-withdrawal-amount.test.ts rename to tests/dao/extensions/actions/aibtc-action-pmt-stx-add-resource.test.ts index 581a99e7..ef55e2a2 100644 --- a/tests/dao/extensions/actions/aibtc-action-set-withdrawal-amount.test.ts +++ b/tests/dao/extensions/actions/aibtc-action-pmt-stx-add-resource.test.ts @@ -18,9 +18,9 @@ const deployer = accounts.get("deployer")!; const address1 = accounts.get("wallet_1")!; const address2 = accounts.get("wallet_2")!; -const contractAddress = `${deployer}.${ContractActionType.DAO_ACTION_SET_WITHDRAWAL_AMOUNT}`; +const contractAddress = `${deployer}.${ContractActionType.DAO_ACTION_PMT_STX_ADD_RESOURCE}`; -describe(`action extension: ${ContractActionType.DAO_ACTION_SET_WITHDRAWAL_AMOUNT}`, () => { +describe(`action extension: ${ContractActionType.DAO_ACTION_PMT_STX_ADD_RESOURCE}`, () => { it("callback() should respond with (ok true)", () => { const callback = simnet.callPublicFn( contractAddress, @@ -32,18 +32,28 @@ describe(`action extension: ${ContractActionType.DAO_ACTION_SET_WITHDRAWAL_AMOUN }); it("run() fails if called directly", () => { - const withdrawalAmount = Cl.uint(1000); + const resourceInfo = { + name: Cl.stringUtf8("test"), + description: Cl.stringUtf8("test description"), + price: Cl.uint(1), + url: Cl.some(Cl.stringUtf8("https://aibtc.dev")), + }; const receipt = simnet.callPublicFn( contractAddress, "run", - [Cl.buffer(Cl.serialize(withdrawalAmount))], + [Cl.buffer(Cl.serialize(Cl.tuple(resourceInfo)))], deployer ); expect(receipt.result).toBeErr(Cl.uint(ActionErrCode.ERR_UNAUTHORIZED)); }); it("run() succeeds if called as a DAO action proposal", () => { - const withdrawalAmount = Cl.uint(1000); + const resourceInfo = { + name: Cl.stringUtf8("test"), + description: Cl.stringUtf8("test description"), + price: Cl.uint(1), + url: Cl.some(Cl.stringUtf8("https://aibtc.dev")), + }; // setup contract names const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; @@ -74,7 +84,7 @@ describe(`action extension: ${ContractActionType.DAO_ACTION_SET_WITHDRAWAL_AMOUN actionProposalsContractAddress, contractAddress, 1, // proposal ID - withdrawalAmount, + Cl.tuple(resourceInfo), deployer, deployer, [deployer, address1, address2], diff --git a/tests/dao/extensions/actions/aibtc-action-set-withdrawal-period.test.ts b/tests/dao/extensions/actions/aibtc-action-pmt-stx-toggle-resource.test.ts similarity index 82% rename from tests/dao/extensions/actions/aibtc-action-set-withdrawal-period.test.ts rename to tests/dao/extensions/actions/aibtc-action-pmt-stx-toggle-resource.test.ts index 54d542a9..916d8838 100644 --- a/tests/dao/extensions/actions/aibtc-action-set-withdrawal-period.test.ts +++ b/tests/dao/extensions/actions/aibtc-action-pmt-stx-toggle-resource.test.ts @@ -18,9 +18,9 @@ const deployer = accounts.get("deployer")!; const address1 = accounts.get("wallet_1")!; const address2 = accounts.get("wallet_2")!; -const contractAddress = `${deployer}.${ContractActionType.DAO_ACTION_SET_WITHDRAWAL_PERIOD}`; +const contractAddress = `${deployer}.${ContractActionType.DAO_ACTION_PMT_STX_TOGGLE_RESOURCE}`; -describe(`action extension: ${ContractActionType.DAO_ACTION_SET_WITHDRAWAL_PERIOD}`, () => { +describe(`action extension: ${ContractActionType.DAO_ACTION_PMT_STX_TOGGLE_RESOURCE}`, () => { it("callback() should respond with (ok true)", () => { const callback = simnet.callPublicFn( contractAddress, @@ -32,18 +32,18 @@ describe(`action extension: ${ContractActionType.DAO_ACTION_SET_WITHDRAWAL_PERIO }); it("run() fails if called directly", () => { - const withdrawalPeriod = Cl.uint(1000); + const resourceName = Cl.stringUtf8("test"); const receipt = simnet.callPublicFn( contractAddress, "run", - [Cl.buffer(Cl.serialize(withdrawalPeriod))], + [Cl.buffer(Cl.serialize(resourceName))], deployer ); expect(receipt.result).toBeErr(Cl.uint(ActionErrCode.ERR_UNAUTHORIZED)); }); it("run() succeeds if called as a DAO action proposal", () => { - const withdrawalPeriod = Cl.uint(1000); + const resourceName = Cl.stringUtf8("test"); // setup contract names const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; @@ -74,13 +74,14 @@ describe(`action extension: ${ContractActionType.DAO_ACTION_SET_WITHDRAWAL_PERIO actionProposalsContractAddress, contractAddress, 1, // proposal ID - withdrawalPeriod, + resourceName, deployer, deployer, [deployer, address1, address2], votingConfig ); - - expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + // result is false because action could not run in this state + // err ERR_RESOURCE_NOT_FOUND u5005 is thrown because resourceName does not exist + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(false)); }); }); diff --git a/tests/dao/extensions/actions/aibtc-action-allow-asset.test.ts b/tests/dao/extensions/actions/aibtc-action-treasury-allow-asset.test.ts similarity index 100% rename from tests/dao/extensions/actions/aibtc-action-allow-asset.test.ts rename to tests/dao/extensions/actions/aibtc-action-treasury-allow-asset.test.ts diff --git a/tests/dao/extensions/aibtc-action-proposals-v2.test.ts b/tests/dao/extensions/aibtc-action-proposals-v2.test.ts index b4188ea1..3282d849 100644 --- a/tests/dao/extensions/aibtc-action-proposals-v2.test.ts +++ b/tests/dao/extensions/aibtc-action-proposals-v2.test.ts @@ -53,6 +53,9 @@ const coreProposalV2VoteSettings = // import contract error codes const ErrCode = ActionProposalsV2ErrCode; +// generic context for creating proposals +const memoContext = "Can pass up to 1024 characters for additional context."; + // helper for getting start block for proposals const getProposalStartBlock = (burnBlockHeight: number): number => { return burnBlockHeight + actionProposalV2VoteSettings.votingDelay; @@ -199,7 +202,11 @@ describe(`public functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const receipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), Cl.bufferFromAscii("test")], + [ + Cl.principal(actionProposalContractAddress), + Cl.bufferFromAscii("test"), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_FETCHING_TOKEN_DATA)); @@ -235,7 +242,11 @@ describe(`public functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const receipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), Cl.bufferFromAscii("test")], + [ + Cl.principal(actionProposalContractAddress), + Cl.bufferFromAscii("test"), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_INVALID_ACTION)); @@ -271,7 +282,11 @@ describe(`public functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const receipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), Cl.bufferFromAscii("test")], + [ + Cl.principal(actionProposalContractAddress), + Cl.bufferFromAscii("test"), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_INVALID_ACTION)); @@ -308,7 +323,11 @@ describe(`public functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const receipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), Cl.bufferFromAscii("test")], + [ + Cl.principal(actionProposalContractAddress), + Cl.bufferFromAscii("test"), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_INVALID_ACTION)); @@ -336,14 +355,18 @@ describe(`public functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const receipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), Cl.bufferFromAscii("test")], + [ + Cl.principal(actionProposalContractAddress), + Cl.bufferFromAscii("test"), + Cl.some(Cl.stringAscii(memoContext)), + ], address1 ); expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_INSUFFICIENT_BALANCE)); }); it("propose-action() fails if more than one proposal is created in a stacks block", () => { - const actionProposalContractAddress2 = `${deployer}.${ContractActionType.DAO_ACTION_ADD_RESOURCE}`; + const actionProposalContractAddress2 = `${deployer}.${ContractActionType.DAO_ACTION_PMT_DAO_ADD_RESOURCE}`; // get dao tokens for deployer, increases liquid tokens const daoTokensReceipt = getDaoTokens( tokenContractAddress, @@ -370,6 +393,7 @@ describe(`public functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { [ Cl.principal(actionProposalContractAddress), Cl.bufferFromAscii("test"), + Cl.some(Cl.stringAscii(memoContext)), ], deployer ), @@ -379,6 +403,7 @@ describe(`public functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { [ Cl.principal(actionProposalContractAddress2), Cl.bufferFromAscii("test2"), + Cl.some(Cl.stringAscii(memoContext)), ], deployer ), @@ -388,6 +413,7 @@ describe(`public functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { [ Cl.principal(actionProposalContractAddress2), Cl.bufferFromAscii("test3"), + Cl.some(Cl.stringAscii(memoContext)), ], deployer ), @@ -443,7 +469,11 @@ describe(`public functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const actionProposalReceipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), Cl.bufferFromAscii("test")], + [ + Cl.principal(actionProposalContractAddress), + Cl.bufferFromAscii("test"), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(actionProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -482,7 +512,11 @@ describe(`public functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const actionProposalReceipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), Cl.bufferFromAscii("test")], + [ + Cl.principal(actionProposalContractAddress), + Cl.bufferFromAscii("test"), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(actionProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -519,7 +553,11 @@ describe(`public functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const actionProposalReceipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), Cl.bufferFromAscii("test")], + [ + Cl.principal(actionProposalContractAddress), + Cl.bufferFromAscii("test"), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(actionProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -561,7 +599,11 @@ describe(`public functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const actionProposalReceipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), Cl.bufferFromAscii("test")], + [ + Cl.principal(actionProposalContractAddress), + Cl.bufferFromAscii("test"), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(actionProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -623,7 +665,11 @@ describe(`public functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const actionProposalReceipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), Cl.bufferFromAscii("test")], + [ + Cl.principal(actionProposalContractAddress), + Cl.bufferFromAscii("test"), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(actionProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -676,7 +722,11 @@ describe(`public functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const actionProposalReceipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), Cl.bufferFromAscii("test")], + [ + Cl.principal(actionProposalContractAddress), + Cl.bufferFromAscii("test"), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(actionProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -715,7 +765,7 @@ describe(`public functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { }); it("conclude-proposal() fails if the action does not match the stored action", () => { - const actionProposalContractAddress2 = `${deployer}.${ContractActionType.DAO_ACTION_ADD_RESOURCE}`; + const actionProposalContractAddress2 = `${deployer}.${ContractActionType.DAO_ACTION_PMT_DAO_ADD_RESOURCE}`; const proposalId = 1; // get dao tokens for deployer, increases liquid tokens const daoTokensReceipt = getDaoTokens( @@ -738,7 +788,11 @@ describe(`public functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const actionProposalReceipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), Cl.bufferFromAscii("test")], + [ + Cl.principal(actionProposalContractAddress), + Cl.bufferFromAscii("test"), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(actionProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -779,7 +833,11 @@ describe(`public functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const actionProposalReceipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), Cl.bufferFromAscii("test")], + [ + Cl.principal(actionProposalContractAddress), + Cl.bufferFromAscii("test"), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(actionProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -834,7 +892,11 @@ describe(`public functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const actionProposalReceipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), Cl.bufferFromAscii("test")], + [ + Cl.principal(actionProposalContractAddress), + Cl.bufferFromAscii("test"), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(actionProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -934,7 +996,11 @@ describe(`read-only functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const actionProposalReceipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), Cl.bufferFromAscii("test")], + [ + Cl.principal(actionProposalContractAddress), + Cl.bufferFromAscii("test"), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(actionProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -995,7 +1061,11 @@ describe(`read-only functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const actionProposalReceipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), actionProposalData], + [ + Cl.principal(actionProposalContractAddress), + actionProposalData, + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(actionProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -1028,6 +1098,7 @@ describe(`read-only functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { concluded: Cl.bool(false), createdAt: Cl.uint(createdAtStacksBlock), creator: Cl.principal(deployer), + memo: Cl.some(Cl.stringAscii(memoContext)), bond: Cl.uint(proposalBond), endBlock: Cl.uint(endBlock), executed: Cl.bool(false), @@ -1106,7 +1177,11 @@ describe(`read-only functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const actionProposalReceipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), actionProposalData], + [ + Cl.principal(actionProposalContractAddress), + actionProposalData, + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(actionProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -1151,7 +1226,11 @@ describe(`read-only functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { //////////////////////////////////////// it("get-total-proposals() returns 0 if no proposals exist", () => { - const expectedResult = Cl.uint(0); + const expectedResult = Cl.tuple({ + total: Cl.uint(0), + concluded: Cl.uint(0), + executed: Cl.uint(0), + }); const receipt = simnet.callReadOnlyFn( actionProposalsV2ContractAddress, "get-total-proposals", @@ -1162,8 +1241,13 @@ describe(`read-only functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { }); it("get-total-proposals() returns total number of proposals", () => { - const actionProposalData = Cl.bufferFromAscii("test"); + const actionProposalData = Cl.buffer( + Cl.serialize(Cl.stringAscii("this is a test")) + ); let totalProposals = 0; + let totalConcludedProposals = 0; + let totalExecutedProposals = 0; + // get dao tokens for deployer, increases liquid tokens const daoTokensReceipt = getDaoTokens( tokenContractAddress, @@ -1185,7 +1269,11 @@ describe(`read-only functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const actionProposalReceipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), actionProposalData], + [ + Cl.principal(actionProposalContractAddress), + actionProposalData, + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(actionProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -1197,14 +1285,24 @@ describe(`read-only functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { [], deployer ); - expect(receipt.result).toStrictEqual(Cl.uint(totalProposals)); + expect(receipt.result).toStrictEqual( + Cl.tuple({ + total: Cl.uint(totalProposals), + concluded: Cl.uint(totalConcludedProposals), + executed: Cl.uint(totalExecutedProposals), + }) + ); // progress the chain simnet.mineEmptyBlock(); // create 2nd proposal const actionProposalReceipt2 = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), actionProposalData], + [ + Cl.principal(actionProposalContractAddress), + actionProposalData, + Cl.none(), + ], deployer ); expect(actionProposalReceipt2.result).toBeOk(Cl.bool(true)); @@ -1216,14 +1314,24 @@ describe(`read-only functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { [], deployer ); - expect(receipt2.result).toStrictEqual(Cl.uint(totalProposals)); + expect(receipt2.result).toStrictEqual( + Cl.tuple({ + total: Cl.uint(totalProposals), + concluded: Cl.uint(totalConcludedProposals), + executed: Cl.uint(totalExecutedProposals), + }) + ); // create 10 proposals for (let i = 0; i < 10; i++) { simnet.mineEmptyBlock(); const actionProposalReceipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), actionProposalData], + [ + Cl.principal(actionProposalContractAddress), + actionProposalData, + i % 2 === 0 ? Cl.some(Cl.stringAscii(memoContext)) : Cl.none(), + ], deployer ); expect(actionProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -1236,7 +1344,70 @@ describe(`read-only functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { [], deployer ); - expect(receipt3.result).toStrictEqual(Cl.uint(totalProposals)); + expect(receipt3.result).toStrictEqual( + Cl.tuple({ + total: Cl.uint(totalProposals), + concluded: Cl.uint(totalConcludedProposals), + executed: Cl.uint(totalExecutedProposals), + }) + ); + // vote on half the proposals + simnet.mineEmptyBurnBlocks(actionProposalV2VoteSettings.votingDelay); + for (let i = 0; i < 12; i++) { + if (i % 2 === 0) { + // vote on proposal + const voteReceipt = simnet.callPublicFn( + actionProposalsV2ContractAddress, + "vote-on-proposal", + [Cl.uint(i + 1), Cl.bool(true)], + deployer + ); + dbgLog(JSON.stringify(cvToValue(voteReceipt.result), null, 2), { + forceLog: true, + titleBefore: `vote on proposal ${i + 1} result`, + }); + expect(voteReceipt.result).toBeOk(Cl.bool(true)); + } + } + // conclude all proposals + simnet.mineEmptyBurnBlocks( + actionProposalV2VoteSettings.votingPeriod + // voting period + actionProposalV2VoteSettings.votingDelay // delay before execution + ); + for (let i = 0; i < 12; i++) { + // conclude proposal + const actionProposalReceipt = simnet.callPublicFn( + actionProposalsV2ContractAddress, + "conclude-proposal", + [Cl.uint(i + 1), Cl.principal(actionProposalContractAddress)], + deployer + ); + dbgLog(JSON.stringify(actionProposalReceipt, null, 2), { + forceLog: true, + titleBefore: `conclude proposal ${i + 1} result`, + }); + if (i % 2 === 0) { + expect(actionProposalReceipt.result).toBeOk(Cl.bool(true)); + totalExecutedProposals++; + } else { + expect(actionProposalReceipt.result).toBeOk(Cl.bool(false)); + } + totalConcludedProposals++; + } + // get total proposals + const receipt4 = simnet.callReadOnlyFn( + actionProposalsV2ContractAddress, + "get-total-proposals", + [], + deployer + ); + expect(receipt4.result).toStrictEqual( + Cl.tuple({ + total: Cl.uint(totalProposals), + concluded: Cl.uint(totalConcludedProposals), + executed: Cl.uint(totalExecutedProposals), + }) + ); }); //////////////////////////////////////// @@ -1392,7 +1563,11 @@ describe(`read-only functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const actionProposalReceipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), actionProposalData], + [ + Cl.principal(actionProposalContractAddress), + actionProposalData, + Cl.none(), + ], deployer ); expect(actionProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -1421,7 +1596,11 @@ describe(`read-only functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const actionProposalReceipt2 = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), actionProposalData], + [ + Cl.principal(actionProposalContractAddress), + actionProposalData, + Cl.none(), + ], deployer ); expect(actionProposalReceipt2.result).toBeOk(Cl.bool(true)); @@ -1453,7 +1632,11 @@ describe(`read-only functions: ${ContractType.DAO_ACTION_PROPOSALS_V2}`, () => { const actionProposalReceipt = simnet.callPublicFn( actionProposalsV2ContractAddress, "propose-action", - [Cl.principal(actionProposalContractAddress), actionProposalData], + [ + Cl.principal(actionProposalContractAddress), + actionProposalData, + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(actionProposalReceipt.result).toBeOk(Cl.bool(true)); diff --git a/tests/dao/extensions/aibtc-core-proposals-v2.test.ts b/tests/dao/extensions/aibtc-core-proposals-v2.test.ts index 97ef4762..0317f582 100644 --- a/tests/dao/extensions/aibtc-core-proposals-v2.test.ts +++ b/tests/dao/extensions/aibtc-core-proposals-v2.test.ts @@ -47,6 +47,9 @@ const coreProposalV2VoteSettings = // import contract error codes const ErrCode = CoreProposalV2ErrCode; +// generic context for creating proposals +const memoContext = "Can pass up to 1024 characters for additional context."; + // helper for getting start block for proposals const getProposalStartBlock = (burnBlockHeight: number): number => { return burnBlockHeight + coreProposalV2VoteSettings.votingDelay; @@ -188,7 +191,10 @@ describe(`public functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { const receipt = simnet.callPublicFn( coreProposalsV2ContractAddress, "create-proposal", - [Cl.principal(coreProposalContactAddress)], + [ + Cl.principal(coreProposalContactAddress), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_FETCHING_TOKEN_DATA)); @@ -216,7 +222,10 @@ describe(`public functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { const receipt = simnet.callPublicFn( coreProposalsV2ContractAddress, "create-proposal", - [Cl.principal(coreProposalContactAddress)], + [ + Cl.principal(coreProposalContactAddress), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_FIRST_VOTING_PERIOD)); @@ -244,7 +253,10 @@ describe(`public functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { const receipt = simnet.callPublicFn( coreProposalsV2ContractAddress, "create-proposal", - [Cl.principal(coreProposalContactAddress)], + [ + Cl.principal(coreProposalContactAddress), + Cl.some(Cl.stringAscii(memoContext)), + ], address1 ); expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_INSUFFICIENT_BALANCE)); @@ -279,7 +291,10 @@ describe(`public functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { const receipt = simnet.callPublicFn( coreProposalsV2ContractAddress, "create-proposal", - [Cl.principal(coreProposalContactAddress)], + [ + Cl.principal(coreProposalContactAddress), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(receipt.result).toBeErr( @@ -326,7 +341,10 @@ describe(`public functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { const createProposalReceipt = simnet.callPublicFn( coreProposalsV2ContractAddress, "create-proposal", - [Cl.principal(coreProposalContactAddress)], + [ + Cl.principal(coreProposalContactAddress), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(createProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -399,7 +417,10 @@ describe(`public functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { const createProposalReceipt = simnet.callPublicFn( coreProposalsV2ContractAddress, "create-proposal", - [Cl.principal(coreProposalContactAddress)], + [ + Cl.principal(coreProposalContactAddress), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(createProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -435,7 +456,10 @@ describe(`public functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { const createProposalReceipt = simnet.callPublicFn( coreProposalsV2ContractAddress, "create-proposal", - [Cl.principal(coreProposalContactAddress)], + [ + Cl.principal(coreProposalContactAddress), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(createProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -476,7 +500,10 @@ describe(`public functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { const createProposalReceipt = simnet.callPublicFn( coreProposalsV2ContractAddress, "create-proposal", - [Cl.principal(coreProposalContactAddress)], + [ + Cl.principal(coreProposalContactAddress), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(createProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -576,7 +603,10 @@ describe(`public functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { const createProposalReceipt = simnet.callPublicFn( coreProposalsV2ContractAddress, "create-proposal", - [Cl.principal(coreProposalContactAddress)], + [ + Cl.principal(coreProposalContactAddress), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(createProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -614,7 +644,10 @@ describe(`public functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { const createProposalReceipt = simnet.callPublicFn( coreProposalsV2ContractAddress, "create-proposal", - [Cl.principal(coreProposalContactAddress)], + [ + Cl.principal(coreProposalContactAddress), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(createProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -687,7 +720,10 @@ describe(`public functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { const createProposalReceipt = simnet.callPublicFn( coreProposalsV2ContractAddress, "create-proposal", - [Cl.principal(coreProposalContactAddress)], + [ + Cl.principal(coreProposalContactAddress), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(createProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -842,7 +878,10 @@ describe(`read-only functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { const createProposalReceipt = simnet.callPublicFn( coreProposalsV2ContractAddress, "create-proposal", - [Cl.principal(coreProposalContactAddress)], + [ + Cl.principal(coreProposalContactAddress), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(createProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -873,6 +912,7 @@ describe(`read-only functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { createdAt: Cl.uint(createdAtStacksBlock), // createdAt caller: Cl.principal(deployer), creator: Cl.principal(deployer), + memo: Cl.some(Cl.stringAscii(memoContext)), bond: Cl.uint(proposalBond), startBlock: Cl.uint(startBlock), // createdAt + coreProposalV2VoteSettings.votingDelay endBlock: Cl.uint(endBlock), // createdAt + coreProposalV2VoteSettings.votingDelay + coreProposalV2VoteSettings.votingPeriod @@ -951,7 +991,10 @@ describe(`read-only functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { const coreProposalReceipt = simnet.callPublicFn( coreProposalsV2ContractAddress, "create-proposal", - [Cl.principal(coreProposalContactAddress)], + [ + Cl.principal(coreProposalContactAddress), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(coreProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -996,17 +1039,26 @@ describe(`read-only functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { //////////////////////////////////////// it("get-total-proposals() returns 0 if no proposals exist", () => { + const expectedResult = Cl.tuple({ + total: Cl.uint(0), + concluded: Cl.uint(0), + executed: Cl.uint(0), + }); const receipt = simnet.callReadOnlyFn( coreProposalsV2ContractAddress, "get-total-proposals", [], deployer ); - expect(receipt.result).toBeUint(0); + expect(receipt.result).toStrictEqual(expectedResult); }); it("get-total-proposals() returns the total number of proposals", () => { - const expectedProposals = 1; + // Track counts + let totalProposals = 0; + let totalConcludedProposals = 0; + let totalExecutedProposals = 0; + // get dao tokens for deployer, increases liquid tokens const daoTokensReceipt = getDaoTokens( tokenContractAddress, @@ -1015,6 +1067,7 @@ describe(`read-only functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { 1000 ); expect(daoTokensReceipt.result).toBeOk(Cl.bool(true)); + // construct DAO const constructReceipt = constructDao( deployer, @@ -1022,16 +1075,23 @@ describe(`read-only functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { bootstrapContractAddress ); expect(constructReceipt.result).toBeOk(Cl.bool(true)); + // progress the chain past the first voting period simnet.mineEmptyBlocks(coreProposalV2VoteSettings.votingPeriod); - // create proposal + + // create initial proposal const coreProposalReceipt = simnet.callPublicFn( coreProposalsV2ContractAddress, "create-proposal", - [Cl.principal(coreProposalContactAddress)], + [ + Cl.principal(coreProposalContactAddress), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(coreProposalReceipt.result).toBeOk(Cl.bool(true)); + totalProposals++; + // get total proposals const receipt = simnet.callReadOnlyFn( coreProposalsV2ContractAddress, @@ -1039,39 +1099,125 @@ describe(`read-only functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { [], deployer ); - expect(receipt.result).toBeUint(expectedProposals); - // create 10 proposals + expect(receipt.result).toStrictEqual( + Cl.tuple({ + total: Cl.uint(totalProposals), + concluded: Cl.uint(totalConcludedProposals), + executed: Cl.uint(totalExecutedProposals), + }) + ); + + // create 10 more proposals const coreProposals = [ getContract(ContractProposalType.DAO_ACTION_PROPOSALS_SET_PROPOSAL_BOND), - getContract(ContractProposalType.DAO_TIMED_VAULT_WITHDRAW_STX), - getContract(ContractProposalType.DAO_TIMED_VAULT_SET_ACCOUNT_HOLDER), getContract(ContractProposalType.DAO_BASE_ADD_NEW_EXTENSION), getContract(ContractProposalType.DAO_BASE_DISABLE_EXTENSION), - getContract(ContractProposalType.DAO_PAYMENTS_INVOICES_ADD_RESOURCE), - getContract( - ContractProposalType.DAO_PAYMENTS_INVOICES_SET_PAYMENT_ADDRESS - ), + getContract(ContractProposalType.DAO_PMT_DAO_ADD_RESOURCE), + getContract(ContractProposalType.DAO_PMT_DAO_SET_PAYMENT_ADDRESS), + getContract(ContractProposalType.DAO_PMT_SBTC_SET_PAYMENT_ADDRESS), + getContract(ContractProposalType.DAO_TIMED_VAULT_STX_SET_ACCOUNT_HOLDER), getContract(ContractProposalType.DAO_TOKEN_OWNER_SET_TOKEN_URI), getContract(ContractProposalType.DAO_TREASURY_ALLOW_ASSET), getContract(ContractProposalType.DAO_TREASURY_DELEGATE_STX), ]; - for (let i = 0; i < 10; i++) { + + for (let i = 0; i < coreProposals.length; i++) { + // Progress chain to avoid block height conflicts + simnet.mineEmptyBlock(); + const coreProposalReceipt = simnet.callPublicFn( coreProposalsV2ContractAddress, "create-proposal", - [Cl.principal(coreProposals[i])], + [ + Cl.principal(coreProposals[i]), + i % 2 === 0 ? Cl.some(Cl.stringAscii(memoContext)) : Cl.none(), + ], deployer ); expect(coreProposalReceipt.result).toBeOk(Cl.bool(true)); - // get total proposals - const receipt = simnet.callReadOnlyFn( + totalProposals++; + } + + // Verify total proposals count + const receipt2 = simnet.callReadOnlyFn( + coreProposalsV2ContractAddress, + "get-total-proposals", + [], + deployer + ); + expect(receipt2.result).toStrictEqual( + Cl.tuple({ + total: Cl.uint(totalProposals), + concluded: Cl.uint(totalConcludedProposals), + executed: Cl.uint(totalExecutedProposals), + }) + ); + + // Vote on half the proposals + simnet.mineEmptyBlocks(coreProposalV2VoteSettings.votingDelay); + for (let i = 0; i < totalProposals; i++) { + if (i % 2 === 0) { + // Vote on proposal with even index + const proposalContract = + i === 0 ? coreProposalContactAddress : coreProposals[i - 1]; + + const voteReceipt = simnet.callPublicFn( + coreProposalsV2ContractAddress, + "vote-on-proposal", + [Cl.principal(proposalContract), Cl.bool(true)], + deployer + ); + expect(voteReceipt.result).toBeOk(Cl.bool(true)); + } + } + + // Conclude all proposals + simnet.mineEmptyBlocks( + coreProposalV2VoteSettings.votingPeriod + + coreProposalV2VoteSettings.votingDelay + ); + + for (let i = 0; i < totalProposals; i++) { + const proposalContract = + i === 0 ? coreProposalContactAddress : coreProposals[i - 1]; + + const concludeReceipt = simnet.callPublicFn( coreProposalsV2ContractAddress, - "get-total-proposals", - [], + "conclude-proposal", + [Cl.principal(proposalContract)], deployer ); - expect(receipt.result).toBeUint(i + 2); + + dbgLog(JSON.stringify(concludeReceipt, null, 2), { + forceLog: true, + titleBefore: `conclude-proposal ${i}`, + }); + + if (i % 2 === 0) { + // Proposals with even indices were voted on, should execute + expect(concludeReceipt.result).toBeOk(Cl.bool(true)); + totalExecutedProposals++; + } else { + // Proposals with odd indices were not voted on, should not execute + expect(concludeReceipt.result).toBeOk(Cl.bool(false)); + } + totalConcludedProposals++; } + + // Verify final counts + const finalReceipt = simnet.callReadOnlyFn( + coreProposalsV2ContractAddress, + "get-total-proposals", + [], + deployer + ); + expect(finalReceipt.result).toStrictEqual( + Cl.tuple({ + total: Cl.uint(totalProposals), + concluded: Cl.uint(totalConcludedProposals), + executed: Cl.uint(totalExecutedProposals), + }) + ); }); //////////////////////////////////////// @@ -1121,7 +1267,10 @@ describe(`read-only functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { const coreProposalReceipt = simnet.callPublicFn( coreProposalsV2ContractAddress, "create-proposal", - [Cl.principal(coreProposalContactAddress)], + [ + Cl.principal(coreProposalContactAddress), + Cl.some(Cl.stringAscii(memoContext)), + ], deployer ); expect(coreProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -1157,7 +1306,7 @@ describe(`read-only functions: ${ContractType.DAO_CORE_PROPOSALS_V2}`, () => { const coreProposalReceipt2 = simnet.callPublicFn( coreProposalsV2ContractAddress, "create-proposal", - [Cl.principal(coreProposalContractAddress2)], + [Cl.principal(coreProposalContractAddress2), Cl.none()], deployer ); expect(coreProposalReceipt2.result).toBeOk(Cl.bool(true)); diff --git a/tests/dao/extensions/aibtc-payment-processor-dao.test.ts b/tests/dao/extensions/aibtc-payment-processor-dao.test.ts new file mode 100644 index 00000000..9180a548 --- /dev/null +++ b/tests/dao/extensions/aibtc-payment-processor-dao.test.ts @@ -0,0 +1,756 @@ +import { Cl, cvToValue, ClarityValue, UIntCV } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { PaymentProcessorErrCode } from "../../error-codes"; +import { ContractType, ContractProposalType } from "../../dao-types"; +import { + constructDao, + dbgLog, + fundVoters, + passCoreProposal, + VOTING_CONFIG, +} from "../../test-utilities"; + +// Contract names for reuse +const accounts = simnet.getAccounts(); +const address1 = accounts.get("wallet_1")!; +const address2 = accounts.get("wallet_2")!; +const deployer = accounts.get("deployer")!; + +const contractAddress = `${deployer}.${ContractType.DAO_PAYMENT_PROCESSOR_DAO}`; +const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; +const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; +const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; +const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; +const coreProposalsContractAddress = `${deployer}.${ContractType.DAO_CORE_PROPOSALS_V2}`; +const proposalContractAddress = `${deployer}.${ContractProposalType.DAO_PMT_DAO_ADD_RESOURCE}`; + +const ErrCode = PaymentProcessorErrCode; + +// Test resource data +const resourceName = "example-resource"; +const resourceDescription = "An example resource"; +const resourcePrice = 100000000000; // 1,000 DAO tokens (8 decimals) +const resourceUrl = "https://example.com"; + +// Helper function to set up a test with a resource and optionally a user +function setupTest(createUser = false) { + // Setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_CORE_PROPOSALS_V2]; + + // Fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // Construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // Pass proposal to add resource + const concludeProposalReceipt = passCoreProposal( + coreProposalsContractAddress, + proposalContractAddress, + deployer, + [deployer, address1, address2], + votingConfig + ); + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + + if (createUser) { + // Pay an invoice + const payInvoice = simnet.callPublicFn( + contractAddress, + "pay-invoice", + [Cl.uint(1), Cl.none()], + address1 + ); + expect(payInvoice.result).toBeOk(Cl.uint(1)); + } +} + +describe(`public functions: ${ContractType.DAO_PAYMENT_PROCESSOR_DAO}`, () => { + //////////////////////////////////////// + // callback() tests + //////////////////////////////////////// + it("callback() should respond with (ok true)", () => { + const callback = simnet.callPublicFn( + contractAddress, + "callback", + [Cl.principal(deployer), Cl.bufferFromAscii("test")], + deployer + ); + expect(callback.result).toBeOk(Cl.bool(true)); + }); + + //////////////////////////////////////// + // set-payment-address() tests + //////////////////////////////////////// + it("set-payment-address() fails if called directly", () => { + const setPaymentAddress = simnet.callPublicFn( + contractAddress, + "set-payment-address", + [Cl.principal(address1)], + deployer + ); + expect(setPaymentAddress.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION) + ); + }); + + //////////////////////////////////////// + // add-resource() tests + //////////////////////////////////////// + it("add-resource() fails if called directly", () => { + const addResource = simnet.callPublicFn( + contractAddress, + "add-resource", + [ + Cl.stringUtf8(resourceName), + Cl.stringUtf8(resourceDescription), + Cl.uint(resourcePrice), + Cl.some(Cl.stringUtf8(resourceUrl)), + ], + deployer + ); + expect(addResource.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION) + ); + }); + + //////////////////////////////////////// + // toggle-resource() tests + //////////////////////////////////////// + it("toggle-resource() fails if called directly", () => { + // NOTE: full check would pass and add one first + const toggleResource = simnet.callPublicFn( + contractAddress, + "toggle-resource", + [Cl.uint(1)], + deployer + ); + expect(toggleResource.result).toBeErr( + Cl.uint(ErrCode.ERR_RESOURCE_NOT_FOUND) + ); + }); + + //////////////////////////////////////// + // toggle-resource-by-name() tests + //////////////////////////////////////// + it("toggle-resource-by-name() fails if called directly", () => { + // NOTE: full check would pass and add one first + const toggleResourceByName = simnet.callPublicFn( + contractAddress, + "toggle-resource-by-name", + [Cl.stringUtf8(resourceName)], + deployer + ); + expect(toggleResourceByName.result).toBeErr( + Cl.uint(ErrCode.ERR_RESOURCE_NOT_FOUND) + ); + }); + + //////////////////////////////////////// + // pay-invoice() tests + //////////////////////////////////////// + it("pay-invoice() fails if resource is not found", () => { + const payInvoice = simnet.callPublicFn( + contractAddress, + "pay-invoice", + [Cl.uint(1), Cl.none()], + address1 + ); + expect(payInvoice.result).toBeErr(Cl.uint(ErrCode.ERR_RESOURCE_NOT_FOUND)); + }); + + it("pay-invoice() fails if resource index is 0", () => { + const payInvoice = simnet.callPublicFn( + contractAddress, + "pay-invoice", + [Cl.uint(0), Cl.none()], + address1 + ); + expect(payInvoice.result).toBeErr(Cl.uint(ErrCode.ERR_RESOURCE_NOT_FOUND)); + }); + + //////////////////////////////////////// + // pay-invoice-by-resource-name() tests + //////////////////////////////////////// + it("pay-invoice-by-resource-name() fails if resource is not found", () => { + const payInvoiceByName = simnet.callPublicFn( + contractAddress, + "pay-invoice-by-resource-name", + [Cl.stringUtf8(resourceName), Cl.none()], + address1 + ); + expect(payInvoiceByName.result).toBeErr( + Cl.uint(ErrCode.ERR_RESOURCE_NOT_FOUND) + ); + }); +}); + +describe(`read-only functions: ${ContractType.DAO_PAYMENT_PROCESSOR_DAO}`, () => { + ///////////////////////////////////////////// + // get-total-users() tests + ///////////////////////////////////////////// + it("get-total-users() returns the total number of users before any are created", () => { + const getTotalUsers = simnet.callReadOnlyFn( + contractAddress, + "get-total-users", + [], + deployer + ).result; + expect(getTotalUsers).toStrictEqual(Cl.uint(0)); + }); + + it("get-total-users() returns the correct count after a user is created", () => { + // Arrange + setupTest(true); + + // Act + const getTotalUsers = simnet.callReadOnlyFn( + contractAddress, + "get-total-users", + [], + deployer + ).result; + + // Assert + expect(getTotalUsers).toStrictEqual(Cl.uint(1)); + }); + + ///////////////////////////////////////////// + // get-total-resources() tests + ///////////////////////////////////////////// + it("get-total-resources() returns the total number of resources before any are created", () => { + const getTotalResources = simnet.callReadOnlyFn( + contractAddress, + "get-total-resources", + [], + deployer + ).result; + expect(getTotalResources).toStrictEqual(Cl.uint(0)); + }); + + it("get-total-resources() returns the correct count after a resource is added", () => { + // Arrange + setupTest(false); + + // Act + const getTotalResources = simnet.callReadOnlyFn( + contractAddress, + "get-total-resources", + [], + deployer + ).result; + + // Assert + expect(getTotalResources).toStrictEqual(Cl.uint(1)); + }); + + ///////////////////////////////////////////// + // get-total-invoices() tests + ///////////////////////////////////////////// + it("get-total-invoices() returns the total number of invoices before any are created", () => { + const getTotalInvoices = simnet.callReadOnlyFn( + contractAddress, + "get-total-invoices", + [], + deployer + ).result; + expect(getTotalInvoices).toStrictEqual(Cl.uint(0)); + }); + + it("get-total-invoices() returns the correct count after an invoice is created", () => { + // Arrange + setupTest(true); + + // Act + const getTotalInvoices = simnet.callReadOnlyFn( + contractAddress, + "get-total-invoices", + [], + deployer + ).result; + + // Assert + expect(getTotalInvoices).toStrictEqual(Cl.uint(1)); + }); + + ///////////////////////////////////////////// + // get-payment-address() tests + ///////////////////////////////////////////// + it("get-payment-address() returns the payment address", () => { + const getPaymentAddress = simnet.callReadOnlyFn( + contractAddress, + "get-payment-address", + [], + deployer + ).result; + // Default payment address should be the treasury + expect(getPaymentAddress).toStrictEqual( + Cl.some(Cl.principal(`${deployer}.aibtc-treasury`)) + ); + }); + + ///////////////////////////////////////////// + // get-total-revenue() tests + ///////////////////////////////////////////// + it("get-total-revenue() returns zero before any payments", () => { + const getTotalRevenue = simnet.callReadOnlyFn( + contractAddress, + "get-total-revenue", + [], + deployer + ).result; + expect(getTotalRevenue).toStrictEqual(Cl.uint(0)); + }); + + it("get-total-revenue() returns the correct total after payment", () => { + // Arrange + setupTest(true); + + // Act + const getTotalRevenue = simnet.callReadOnlyFn( + contractAddress, + "get-total-revenue", + [], + deployer + ).result; + + // Assert + expect(getTotalRevenue).toStrictEqual(Cl.uint(resourcePrice)); + }); + + ///////////////////////////////////////////// + // get-contract-data() tests + ///////////////////////////////////////////// + it("get-contract-data() returns the initial contract data", () => { + const getContractData = simnet.callReadOnlyFn( + contractAddress, + "get-contract-data", + [], + deployer + ).result; + + const expectedData = Cl.tuple({ + contractAddress: Cl.principal(contractAddress), + paymentAddress: Cl.some(Cl.principal(`${deployer}.aibtc-treasury`)), + paymentToken: Cl.principal(tokenContractAddress), + totalInvoices: Cl.uint(0), + totalResources: Cl.uint(0), + totalRevenue: Cl.uint(0), + totalUsers: Cl.uint(0), + }); + + expect(getContractData).toStrictEqual(expectedData); + }); + + it("get-contract-data() returns updated contract data after payment", () => { + // Arrange + setupTest(true); + + // Act + const getContractData = simnet.callReadOnlyFn( + contractAddress, + "get-contract-data", + [], + deployer + ).result; + + // Assert + const expectedData = Cl.tuple({ + contractAddress: Cl.principal(contractAddress), + paymentAddress: Cl.some(Cl.principal(`${deployer}.aibtc-treasury`)), + paymentToken: Cl.principal(tokenContractAddress), + totalInvoices: Cl.uint(1), + totalResources: Cl.uint(1), + totalRevenue: Cl.uint(resourcePrice), + totalUsers: Cl.uint(1), + }); + + expect(getContractData).toStrictEqual(expectedData); + }); + + ///////////////////////////////////////////// + // get-user-index() tests + ///////////////////////////////////////////// + it("get-user-index() returns none for non-existent user", () => { + const getUserIndex = simnet.callReadOnlyFn( + contractAddress, + "get-user-index", + [Cl.principal(address2)], + deployer + ).result; + expect(getUserIndex).toBeNone(); + }); + + it("get-user-index() returns the correct index for existing user", () => { + // Arrange + setupTest(true); + + // Act + const getUserIndex = simnet.callReadOnlyFn( + contractAddress, + "get-user-index", + [Cl.principal(address1)], + deployer + ).result; + + // Assert + expect(getUserIndex).toStrictEqual(Cl.some(Cl.uint(1))); + }); + + ///////////////////////////////////////////// + // get-user-data() tests + ///////////////////////////////////////////// + it("get-user-data() returns none for non-existent user index", () => { + const getUserData = simnet.callReadOnlyFn( + contractAddress, + "get-user-data", + [Cl.uint(999)], + deployer + ).result; + expect(getUserData).toBeNone(); + }); + + it("get-user-data() returns the correct data for existing user", () => { + // Arrange + setupTest(true); + + // Act + const getUserData = simnet.callReadOnlyFn( + contractAddress, + "get-user-data", + [Cl.uint(1)], + deployer + ).result; + + // Assert + const expectedUserData = Cl.tuple({ + address: Cl.principal(address1), + totalSpent: Cl.uint(resourcePrice), + totalUsed: Cl.uint(1), + }); + + expect(getUserData).toStrictEqual(Cl.some(expectedUserData)); + }); + + ///////////////////////////////////////////// + // get-user-data-by-address() tests + ///////////////////////////////////////////// + it("get-user-data-by-address() returns none for non-existent user", () => { + const getUserDataByAddress = simnet.callReadOnlyFn( + contractAddress, + "get-user-data-by-address", + [Cl.principal(address2)], + deployer + ).result; + expect(getUserDataByAddress).toBeNone(); + }); + + it("get-user-data-by-address() returns the correct data for existing user", () => { + // Arrange + setupTest(true); + + // Act + const getUserDataByAddress = simnet.callReadOnlyFn( + contractAddress, + "get-user-data-by-address", + [Cl.principal(address1)], + deployer + ).result; + + // Assert + const expectedUserData = Cl.tuple({ + address: Cl.principal(address1), + totalSpent: Cl.uint(resourcePrice), + totalUsed: Cl.uint(1), + }); + + expect(getUserDataByAddress).toStrictEqual(Cl.some(expectedUserData)); + }); + + ///////////////////////////////////////////// + // get-resource-index() tests + ///////////////////////////////////////////// + it("get-resource-index() returns none for non-existent resource", () => { + const getResourceIndex = simnet.callReadOnlyFn( + contractAddress, + "get-resource-index", + [Cl.stringUtf8("non-existent-resource")], + deployer + ).result; + expect(getResourceIndex).toBeNone(); + }); + + it("get-resource-index() returns the correct index for existing resource", () => { + // Arrange + setupTest(false); + + // Act + const getResourceIndex = simnet.callReadOnlyFn( + contractAddress, + "get-resource-index", + [Cl.stringUtf8(resourceName)], + deployer + ).result; + + // Assert + expect(getResourceIndex).toStrictEqual(Cl.some(Cl.uint(1))); + }); + + ///////////////////////////////////////////// + // get-resource() tests + ///////////////////////////////////////////// + it("get-resource() returns none for non-existent resource index", () => { + const getResource = simnet.callReadOnlyFn( + contractAddress, + "get-resource", + [Cl.uint(999)], + deployer + ).result; + expect(getResource).toBeNone(); + }); + + it("get-resource() returns the correct data for existing resource", () => { + // Arrange + setupTest(false); + + // Act + const resourceRecordCV = simnet.callReadOnlyFn( + contractAddress, + "get-resource", + [Cl.uint(1)], + deployer + ).result; + const resourceRecord = cvToValue(resourceRecordCV).value; + const createdAtCV: UIntCV = { + type: 1, + value: resourceRecord.createdAt.value, + }; + const createdAt = cvToValue(createdAtCV); + // Assert + const expectedResourceData = Cl.tuple({ + name: Cl.stringUtf8(resourceName), + description: Cl.stringUtf8(resourceDescription), + price: Cl.uint(resourcePrice), + enabled: Cl.bool(true), + url: Cl.some(Cl.stringUtf8(resourceUrl)), + createdAt: Cl.uint(createdAt), + totalSpent: Cl.uint(0), + totalUsed: Cl.uint(0), + }); + + expect(resourceRecordCV).toStrictEqual(Cl.some(expectedResourceData)); + }); + + ///////////////////////////////////////////// + // get-resource-by-name() tests + ///////////////////////////////////////////// + it("get-resource-by-name() returns none for non-existent resource", () => { + const getResourceByName = simnet.callReadOnlyFn( + contractAddress, + "get-resource-by-name", + [Cl.stringUtf8("non-existent-resource")], + deployer + ).result; + expect(getResourceByName).toBeNone(); + }); + + it("get-resource-by-name() returns the correct data for existing resource", () => { + // Arrange + setupTest(false); + + // Act + const resourceRecordCV = simnet.callReadOnlyFn( + contractAddress, + "get-resource-by-name", + [Cl.stringUtf8(resourceName)], + deployer + ).result; + const resourceRecord = cvToValue(resourceRecordCV).value; + const createdAtCV: UIntCV = { + type: 1, + value: resourceRecord.createdAt.value, + }; + const createdAt = cvToValue(createdAtCV); + + // Assert + const expectedResourceData = Cl.tuple({ + name: Cl.stringUtf8(resourceName), + description: Cl.stringUtf8(resourceDescription), + price: Cl.uint(resourcePrice), + enabled: Cl.bool(true), + url: Cl.some(Cl.stringUtf8(resourceUrl)), + createdAt: Cl.uint(createdAt), + totalSpent: Cl.uint(0), + totalUsed: Cl.uint(0), + }); + + expect(resourceRecordCV).toStrictEqual(Cl.some(expectedResourceData)); + }); + + ///////////////////////////////////////////// + // get-invoice() tests + ///////////////////////////////////////////// + it("get-invoice() returns none for non-existent invoice index", () => { + const getInvoice = simnet.callReadOnlyFn( + contractAddress, + "get-invoice", + [Cl.uint(999)], + deployer + ).result; + expect(getInvoice).toBeNone(); + }); + + it("get-invoice() returns the correct data for existing invoice", () => { + // Arrange + setupTest(true); + + // Act + const invoiceRecordCV = simnet.callReadOnlyFn( + contractAddress, + "get-invoice", + [Cl.uint(1)], + deployer + ).result; + const invoiceRecord = cvToValue(invoiceRecordCV).value; + const createdAtCV: UIntCV = { + type: 1, + value: invoiceRecord.createdAt.value, + }; + const createdAt = cvToValue(createdAtCV); + + // Assert + const expectedInvoiceData = Cl.tuple({ + amount: Cl.uint(resourcePrice), + userIndex: Cl.uint(1), + resourceIndex: Cl.uint(1), + resourceName: Cl.stringUtf8(resourceName), + createdAt: Cl.uint(createdAt), + }); + + expect(invoiceRecordCV).toStrictEqual(Cl.some(expectedInvoiceData)); + }); + + ///////////////////////////////////////////// + // get-recent-payment() tests + ///////////////////////////////////////////// + it("get-recent-payment() returns none for non-existent user/resource combination", () => { + const getRecentPayment = simnet.callReadOnlyFn( + contractAddress, + "get-recent-payment", + [Cl.uint(999), Cl.uint(999)], + deployer + ).result; + expect(getRecentPayment).toBeNone(); + }); + + it("get-recent-payment() returns the correct invoice index for existing user/resource", () => { + // Arrange + setupTest(true); + + // Act + const getRecentPayment = simnet.callReadOnlyFn( + contractAddress, + "get-recent-payment", + [Cl.uint(1), Cl.uint(1)], + deployer + ).result; + + // Assert + expect(getRecentPayment).toStrictEqual(Cl.some(Cl.uint(1))); + }); + + ///////////////////////////////////////////// + // get-recent-payment-data() tests + ///////////////////////////////////////////// + it("get-recent-payment-data() returns none for non-existent user/resource combination", () => { + const getRecentPaymentData = simnet.callReadOnlyFn( + contractAddress, + "get-recent-payment-data", + [Cl.uint(999), Cl.uint(999)], + deployer + ).result; + expect(getRecentPaymentData).toBeNone(); + }); + + it("get-recent-payment-data() returns the correct data for existing user/resource", () => { + // Arrange + setupTest(true); + + // Act + const paymentDataCV = simnet.callReadOnlyFn( + contractAddress, + "get-recent-payment-data", + [Cl.uint(1), Cl.uint(1)], + deployer + ).result; + const paymentData = cvToValue(paymentDataCV).value; + const createdAtCV: UIntCV = { + type: 1, + value: paymentData.createdAt.value, + }; + const createdAt = cvToValue(createdAtCV); + + // Assert + const expectedInvoiceData = Cl.tuple({ + amount: Cl.uint(resourcePrice), + userIndex: Cl.uint(1), + resourceIndex: Cl.uint(1), + resourceName: Cl.stringUtf8(resourceName), + createdAt: Cl.uint(createdAt), + }); + + expect(paymentDataCV).toStrictEqual(Cl.some(expectedInvoiceData)); + }); + + ///////////////////////////////////////////// + // get-recent-payment-data-by-address() tests + ///////////////////////////////////////////// + it("get-recent-payment-data-by-address() returns none for non-existent user/resource combination", () => { + const getRecentPaymentDataByAddress = simnet.callReadOnlyFn( + contractAddress, + "get-recent-payment-data-by-address", + [Cl.stringUtf8("non-existent-resource"), Cl.principal(address2)], + deployer + ).result; + expect(getRecentPaymentDataByAddress).toBeNone(); + }); + + it("get-recent-payment-data-by-address() returns the correct data for existing user/resource", () => { + // Arrange + setupTest(true); + + // Act + const paymentDataByAddressCV = simnet.callReadOnlyFn( + contractAddress, + "get-recent-payment-data-by-address", + [Cl.stringUtf8(resourceName), Cl.principal(address1)], + deployer + ).result; + const paymentDataByAddress = cvToValue(paymentDataByAddressCV).value; + const createdAtCV: UIntCV = { + type: 1, + value: paymentDataByAddress.createdAt.value, + }; + const createdAt = cvToValue(createdAtCV); + + // Assert + const expectedInvoiceData = Cl.tuple({ + amount: Cl.uint(resourcePrice), + userIndex: Cl.uint(1), + resourceIndex: Cl.uint(1), + resourceName: Cl.stringUtf8(resourceName), + createdAt: Cl.uint(createdAt), + }); + + expect(paymentDataByAddressCV).toStrictEqual(Cl.some(expectedInvoiceData)); + }); +}); diff --git a/tests/dao/extensions/aibtc-payment-processor-sbtc.test.ts b/tests/dao/extensions/aibtc-payment-processor-sbtc.test.ts new file mode 100644 index 00000000..aa577917 --- /dev/null +++ b/tests/dao/extensions/aibtc-payment-processor-sbtc.test.ts @@ -0,0 +1,756 @@ +import { Cl, cvToValue, ClarityValue, UIntCV } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { PaymentProcessorErrCode } from "../../error-codes"; +import { ContractType, ContractProposalType } from "../../dao-types"; +import { + constructDao, + dbgLog, + fundVoters, + passCoreProposal, + VOTING_CONFIG, + SBTC_CONTRACT, +} from "../../test-utilities"; + +const accounts = simnet.getAccounts(); +const address1 = accounts.get("wallet_1")!; +const address2 = accounts.get("wallet_2")!; +const deployer = accounts.get("deployer")!; + +const contractAddress = `${deployer}.${ContractType.DAO_PAYMENT_PROCESSOR_SBTC}`; +const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; +const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; +const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; +const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; +const coreProposalsContractAddress = `${deployer}.${ContractType.DAO_CORE_PROPOSALS_V2}`; +const proposalContractAddress = `${deployer}.${ContractProposalType.DAO_PMT_SBTC_ADD_RESOURCE}`; + +const ErrCode = PaymentProcessorErrCode; + +// Test resource data +const resourceName = "example-resource"; +const resourceDescription = "An example resource"; +const resourcePrice = 1000000; // 0.01 sBTC (8 decimals) +const resourceUrl = "https://example.com"; + +// Helper function to set up a test with a resource and optionally a user +function setupTest(createUser = false) { + // Setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_CORE_PROPOSALS_V2]; + + // Fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // Construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // Pass proposal to add resource + const concludeProposalReceipt = passCoreProposal( + coreProposalsContractAddress, + proposalContractAddress, + deployer, + [deployer, address1, address2], + votingConfig + ); + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + + if (createUser) { + // Pay an invoice + const payInvoice = simnet.callPublicFn( + contractAddress, + "pay-invoice", + [Cl.uint(1), Cl.none()], + address1 + ); + expect(payInvoice.result).toBeOk(Cl.uint(1)); + } +} + +describe(`public functions: ${ContractType.DAO_PAYMENT_PROCESSOR_SBTC}`, () => { + //////////////////////////////////////// + // callback() tests + //////////////////////////////////////// + it("callback() should respond with (ok true)", () => { + const callback = simnet.callPublicFn( + contractAddress, + "callback", + [Cl.principal(deployer), Cl.bufferFromAscii("test")], + deployer + ); + expect(callback.result).toBeOk(Cl.bool(true)); + }); + + //////////////////////////////////////// + // set-payment-address() tests + //////////////////////////////////////// + it("set-payment-address() fails if called directly", () => { + const setPaymentAddress = simnet.callPublicFn( + contractAddress, + "set-payment-address", + [Cl.principal(address1)], + deployer + ); + expect(setPaymentAddress.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION) + ); + }); + + //////////////////////////////////////// + // add-resource() tests + //////////////////////////////////////// + it("add-resource() fails if called directly", () => { + const addResource = simnet.callPublicFn( + contractAddress, + "add-resource", + [ + Cl.stringUtf8(resourceName), + Cl.stringUtf8(resourceDescription), + Cl.uint(resourcePrice), + Cl.some(Cl.stringUtf8(resourceUrl)), + ], + deployer + ); + expect(addResource.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION) + ); + }); + + //////////////////////////////////////// + // toggle-resource() tests + //////////////////////////////////////// + it("toggle-resource() fails if called directly", () => { + // NOTE: full check would pass and add one first + const toggleResource = simnet.callPublicFn( + contractAddress, + "toggle-resource", + [Cl.uint(1)], + deployer + ); + expect(toggleResource.result).toBeErr( + Cl.uint(ErrCode.ERR_RESOURCE_NOT_FOUND) + ); + }); + + //////////////////////////////////////// + // toggle-resource-by-name() tests + //////////////////////////////////////// + it("toggle-resource-by-name() fails if called directly", () => { + // NOTE: full check would pass and add one first + const toggleResourceByName = simnet.callPublicFn( + contractAddress, + "toggle-resource-by-name", + [Cl.stringUtf8(resourceName)], + deployer + ); + expect(toggleResourceByName.result).toBeErr( + Cl.uint(ErrCode.ERR_RESOURCE_NOT_FOUND) + ); + }); + + //////////////////////////////////////// + // pay-invoice() tests + //////////////////////////////////////// + it("pay-invoice() fails if resource is not found", () => { + const payInvoice = simnet.callPublicFn( + contractAddress, + "pay-invoice", + [Cl.uint(1), Cl.none()], + address1 + ); + expect(payInvoice.result).toBeErr(Cl.uint(ErrCode.ERR_RESOURCE_NOT_FOUND)); + }); + + it("pay-invoice() fails if resource index is 0", () => { + const payInvoice = simnet.callPublicFn( + contractAddress, + "pay-invoice", + [Cl.uint(0), Cl.none()], + address1 + ); + expect(payInvoice.result).toBeErr(Cl.uint(ErrCode.ERR_RESOURCE_NOT_FOUND)); + }); + + //////////////////////////////////////// + // pay-invoice-by-resource-name() tests + //////////////////////////////////////// + it("pay-invoice-by-resource-name() fails if resource is not found", () => { + const payInvoiceByName = simnet.callPublicFn( + contractAddress, + "pay-invoice-by-resource-name", + [Cl.stringUtf8(resourceName), Cl.none()], + address1 + ); + expect(payInvoiceByName.result).toBeErr( + Cl.uint(ErrCode.ERR_RESOURCE_NOT_FOUND) + ); + }); +}); + +describe(`read-only functions: ${ContractType.DAO_PAYMENT_PROCESSOR_SBTC}`, () => { + ///////////////////////////////////////////// + // get-total-users() tests + ///////////////////////////////////////////// + it("get-total-users() returns the total number of users before any are created", () => { + const getTotalUsers = simnet.callReadOnlyFn( + contractAddress, + "get-total-users", + [], + deployer + ).result; + expect(getTotalUsers).toStrictEqual(Cl.uint(0)); + }); + + it("get-total-users() returns the correct count after a user is created", () => { + // Arrange + setupTest(true); + + // Act + const getTotalUsers = simnet.callReadOnlyFn( + contractAddress, + "get-total-users", + [], + deployer + ).result; + + // Assert + expect(getTotalUsers).toStrictEqual(Cl.uint(1)); + }); + + ///////////////////////////////////////////// + // get-total-resources() tests + ///////////////////////////////////////////// + it("get-total-resources() returns the total number of resources before any are created", () => { + const getTotalResources = simnet.callReadOnlyFn( + contractAddress, + "get-total-resources", + [], + deployer + ).result; + expect(getTotalResources).toStrictEqual(Cl.uint(0)); + }); + + it("get-total-resources() returns the correct count after a resource is added", () => { + // Arrange + setupTest(false); + + // Act + const getTotalResources = simnet.callReadOnlyFn( + contractAddress, + "get-total-resources", + [], + deployer + ).result; + + // Assert + expect(getTotalResources).toStrictEqual(Cl.uint(1)); + }); + + ///////////////////////////////////////////// + // get-total-invoices() tests + ///////////////////////////////////////////// + it("get-total-invoices() returns the total number of invoices before any are created", () => { + const getTotalInvoices = simnet.callReadOnlyFn( + contractAddress, + "get-total-invoices", + [], + deployer + ).result; + expect(getTotalInvoices).toStrictEqual(Cl.uint(0)); + }); + + it("get-total-invoices() returns the correct count after an invoice is created", () => { + // Arrange + setupTest(true); + + // Act + const getTotalInvoices = simnet.callReadOnlyFn( + contractAddress, + "get-total-invoices", + [], + deployer + ).result; + + // Assert + expect(getTotalInvoices).toStrictEqual(Cl.uint(1)); + }); + + ///////////////////////////////////////////// + // get-payment-address() tests + ///////////////////////////////////////////// + it("get-payment-address() returns the payment address", () => { + const getPaymentAddress = simnet.callReadOnlyFn( + contractAddress, + "get-payment-address", + [], + deployer + ).result; + // Default payment address should be the treasury + expect(getPaymentAddress).toStrictEqual( + Cl.some(Cl.principal(`${deployer}.aibtc-treasury`)) + ); + }); + + ///////////////////////////////////////////// + // get-total-revenue() tests + ///////////////////////////////////////////// + it("get-total-revenue() returns zero before any payments", () => { + const getTotalRevenue = simnet.callReadOnlyFn( + contractAddress, + "get-total-revenue", + [], + deployer + ).result; + expect(getTotalRevenue).toStrictEqual(Cl.uint(0)); + }); + + it("get-total-revenue() returns the correct total after payment", () => { + // Arrange + setupTest(true); + + // Act + const getTotalRevenue = simnet.callReadOnlyFn( + contractAddress, + "get-total-revenue", + [], + deployer + ).result; + + // Assert + expect(getTotalRevenue).toStrictEqual(Cl.uint(resourcePrice)); + }); + + ///////////////////////////////////////////// + // get-contract-data() tests + ///////////////////////////////////////////// + it("get-contract-data() returns the initial contract data", () => { + const getContractData = simnet.callReadOnlyFn( + contractAddress, + "get-contract-data", + [], + deployer + ).result; + + const expectedData = Cl.tuple({ + contractAddress: Cl.principal(contractAddress), + paymentAddress: Cl.some(Cl.principal(`${deployer}.aibtc-treasury`)), + paymentToken: Cl.principal(SBTC_CONTRACT), + totalInvoices: Cl.uint(0), + totalResources: Cl.uint(0), + totalRevenue: Cl.uint(0), + totalUsers: Cl.uint(0), + }); + + expect(getContractData).toStrictEqual(expectedData); + }); + + it("get-contract-data() returns updated contract data after payment", () => { + // Arrange + setupTest(true); + + // Act + const getContractData = simnet.callReadOnlyFn( + contractAddress, + "get-contract-data", + [], + deployer + ).result; + + // Assert + const expectedData = Cl.tuple({ + contractAddress: Cl.principal(contractAddress), + paymentAddress: Cl.some(Cl.principal(`${deployer}.aibtc-treasury`)), + paymentToken: Cl.principal(SBTC_CONTRACT), + totalInvoices: Cl.uint(1), + totalResources: Cl.uint(1), + totalRevenue: Cl.uint(resourcePrice), + totalUsers: Cl.uint(1), + }); + + expect(getContractData).toStrictEqual(expectedData); + }); + + ///////////////////////////////////////////// + // get-user-index() tests + ///////////////////////////////////////////// + it("get-user-index() returns none for non-existent user", () => { + const getUserIndex = simnet.callReadOnlyFn( + contractAddress, + "get-user-index", + [Cl.principal(address2)], + deployer + ).result; + expect(getUserIndex).toBeNone(); + }); + + it("get-user-index() returns the correct index for existing user", () => { + // Arrange + setupTest(true); + + // Act + const getUserIndex = simnet.callReadOnlyFn( + contractAddress, + "get-user-index", + [Cl.principal(address1)], + deployer + ).result; + + // Assert + expect(getUserIndex).toStrictEqual(Cl.some(Cl.uint(1))); + }); + + ///////////////////////////////////////////// + // get-user-data() tests + ///////////////////////////////////////////// + it("get-user-data() returns none for non-existent user index", () => { + const getUserData = simnet.callReadOnlyFn( + contractAddress, + "get-user-data", + [Cl.uint(999)], + deployer + ).result; + expect(getUserData).toBeNone(); + }); + + it("get-user-data() returns the correct data for existing user", () => { + // Arrange + setupTest(true); + + // Act + const getUserData = simnet.callReadOnlyFn( + contractAddress, + "get-user-data", + [Cl.uint(1)], + deployer + ).result; + + // Assert + const expectedUserData = Cl.tuple({ + address: Cl.principal(address1), + totalSpent: Cl.uint(resourcePrice), + totalUsed: Cl.uint(1), + }); + + expect(getUserData).toStrictEqual(Cl.some(expectedUserData)); + }); + + ///////////////////////////////////////////// + // get-user-data-by-address() tests + ///////////////////////////////////////////// + it("get-user-data-by-address() returns none for non-existent user", () => { + const getUserDataByAddress = simnet.callReadOnlyFn( + contractAddress, + "get-user-data-by-address", + [Cl.principal(address2)], + deployer + ).result; + expect(getUserDataByAddress).toBeNone(); + }); + + it("get-user-data-by-address() returns the correct data for existing user", () => { + // Arrange + setupTest(true); + + // Act + const getUserDataByAddress = simnet.callReadOnlyFn( + contractAddress, + "get-user-data-by-address", + [Cl.principal(address1)], + deployer + ).result; + + // Assert + const expectedUserData = Cl.tuple({ + address: Cl.principal(address1), + totalSpent: Cl.uint(resourcePrice), + totalUsed: Cl.uint(1), + }); + + expect(getUserDataByAddress).toStrictEqual(Cl.some(expectedUserData)); + }); + + ///////////////////////////////////////////// + // get-resource-index() tests + ///////////////////////////////////////////// + it("get-resource-index() returns none for non-existent resource", () => { + const getResourceIndex = simnet.callReadOnlyFn( + contractAddress, + "get-resource-index", + [Cl.stringUtf8("non-existent-resource")], + deployer + ).result; + expect(getResourceIndex).toBeNone(); + }); + + it("get-resource-index() returns the correct index for existing resource", () => { + // Arrange + setupTest(false); + + // Act + const getResourceIndex = simnet.callReadOnlyFn( + contractAddress, + "get-resource-index", + [Cl.stringUtf8(resourceName)], + deployer + ).result; + + // Assert + expect(getResourceIndex).toStrictEqual(Cl.some(Cl.uint(1))); + }); + + ///////////////////////////////////////////// + // get-resource() tests + ///////////////////////////////////////////// + it("get-resource() returns none for non-existent resource index", () => { + const getResource = simnet.callReadOnlyFn( + contractAddress, + "get-resource", + [Cl.uint(999)], + deployer + ).result; + expect(getResource).toBeNone(); + }); + + it("get-resource() returns the correct data for existing resource", () => { + // Arrange + setupTest(false); + + // Act + const resourceRecordCV = simnet.callReadOnlyFn( + contractAddress, + "get-resource", + [Cl.uint(1)], + deployer + ).result; + const resourceRecord = cvToValue(resourceRecordCV).value; + const createdAtCV: UIntCV = { + type: 1, + value: resourceRecord.createdAt.value, + }; + const createdAt = cvToValue(createdAtCV); + // Assert + const expectedResourceData = Cl.tuple({ + name: Cl.stringUtf8(resourceName), + description: Cl.stringUtf8(resourceDescription), + price: Cl.uint(resourcePrice), + enabled: Cl.bool(true), + url: Cl.some(Cl.stringUtf8(resourceUrl)), + createdAt: Cl.uint(createdAt), + totalSpent: Cl.uint(0), + totalUsed: Cl.uint(0), + }); + + expect(resourceRecordCV).toStrictEqual(Cl.some(expectedResourceData)); + }); + + ///////////////////////////////////////////// + // get-resource-by-name() tests + ///////////////////////////////////////////// + it("get-resource-by-name() returns none for non-existent resource", () => { + const getResourceByName = simnet.callReadOnlyFn( + contractAddress, + "get-resource-by-name", + [Cl.stringUtf8("non-existent-resource")], + deployer + ).result; + expect(getResourceByName).toBeNone(); + }); + + it("get-resource-by-name() returns the correct data for existing resource", () => { + // Arrange + setupTest(false); + + // Act + const resourceRecordCV = simnet.callReadOnlyFn( + contractAddress, + "get-resource-by-name", + [Cl.stringUtf8(resourceName)], + deployer + ).result; + const resourceRecord = cvToValue(resourceRecordCV).value; + const createdAtCV: UIntCV = { + type: 1, + value: resourceRecord.createdAt.value, + }; + const createdAt = cvToValue(createdAtCV); + + // Assert + const expectedResourceData = Cl.tuple({ + name: Cl.stringUtf8(resourceName), + description: Cl.stringUtf8(resourceDescription), + price: Cl.uint(resourcePrice), + enabled: Cl.bool(true), + url: Cl.some(Cl.stringUtf8(resourceUrl)), + createdAt: Cl.uint(createdAt), + totalSpent: Cl.uint(0), + totalUsed: Cl.uint(0), + }); + + expect(resourceRecordCV).toStrictEqual(Cl.some(expectedResourceData)); + }); + + ///////////////////////////////////////////// + // get-invoice() tests + ///////////////////////////////////////////// + it("get-invoice() returns none for non-existent invoice index", () => { + const getInvoice = simnet.callReadOnlyFn( + contractAddress, + "get-invoice", + [Cl.uint(999)], + deployer + ).result; + expect(getInvoice).toBeNone(); + }); + + it("get-invoice() returns the correct data for existing invoice", () => { + // Arrange + setupTest(true); + + // Act + const invoiceRecordCV = simnet.callReadOnlyFn( + contractAddress, + "get-invoice", + [Cl.uint(1)], + deployer + ).result; + const invoiceRecord = cvToValue(invoiceRecordCV).value; + const createdAtCV: UIntCV = { + type: 1, + value: invoiceRecord.createdAt.value, + }; + const createdAt = cvToValue(createdAtCV); + + // Assert + const expectedInvoiceData = Cl.tuple({ + amount: Cl.uint(resourcePrice), + userIndex: Cl.uint(1), + resourceIndex: Cl.uint(1), + resourceName: Cl.stringUtf8(resourceName), + createdAt: Cl.uint(createdAt), + }); + + expect(invoiceRecordCV).toStrictEqual(Cl.some(expectedInvoiceData)); + }); + + ///////////////////////////////////////////// + // get-recent-payment() tests + ///////////////////////////////////////////// + it("get-recent-payment() returns none for non-existent user/resource combination", () => { + const getRecentPayment = simnet.callReadOnlyFn( + contractAddress, + "get-recent-payment", + [Cl.uint(999), Cl.uint(999)], + deployer + ).result; + expect(getRecentPayment).toBeNone(); + }); + + it("get-recent-payment() returns the correct invoice index for existing user/resource", () => { + // Arrange + setupTest(true); + + // Act + const getRecentPayment = simnet.callReadOnlyFn( + contractAddress, + "get-recent-payment", + [Cl.uint(1), Cl.uint(1)], + deployer + ).result; + + // Assert + expect(getRecentPayment).toStrictEqual(Cl.some(Cl.uint(1))); + }); + + ///////////////////////////////////////////// + // get-recent-payment-data() tests + ///////////////////////////////////////////// + it("get-recent-payment-data() returns none for non-existent user/resource combination", () => { + const getRecentPaymentData = simnet.callReadOnlyFn( + contractAddress, + "get-recent-payment-data", + [Cl.uint(999), Cl.uint(999)], + deployer + ).result; + expect(getRecentPaymentData).toBeNone(); + }); + + it("get-recent-payment-data() returns the correct data for existing user/resource", () => { + // Arrange + setupTest(true); + + // Act + const paymentDataCV = simnet.callReadOnlyFn( + contractAddress, + "get-recent-payment-data", + [Cl.uint(1), Cl.uint(1)], + deployer + ).result; + const paymentData = cvToValue(paymentDataCV).value; + const createdAtCV: UIntCV = { + type: 1, + value: paymentData.createdAt.value, + }; + const createdAt = cvToValue(createdAtCV); + + // Assert + const expectedInvoiceData = Cl.tuple({ + amount: Cl.uint(resourcePrice), + userIndex: Cl.uint(1), + resourceIndex: Cl.uint(1), + resourceName: Cl.stringUtf8(resourceName), + createdAt: Cl.uint(createdAt), + }); + + expect(paymentDataCV).toStrictEqual(Cl.some(expectedInvoiceData)); + }); + + ///////////////////////////////////////////// + // get-recent-payment-data-by-address() tests + ///////////////////////////////////////////// + it("get-recent-payment-data-by-address() returns none for non-existent user/resource combination", () => { + const getRecentPaymentDataByAddress = simnet.callReadOnlyFn( + contractAddress, + "get-recent-payment-data-by-address", + [Cl.stringUtf8("non-existent-resource"), Cl.principal(address2)], + deployer + ).result; + expect(getRecentPaymentDataByAddress).toBeNone(); + }); + + it("get-recent-payment-data-by-address() returns the correct data for existing user/resource", () => { + // Arrange + setupTest(true); + + // Act + const paymentDataByAddressCV = simnet.callReadOnlyFn( + contractAddress, + "get-recent-payment-data-by-address", + [Cl.stringUtf8(resourceName), Cl.principal(address1)], + deployer + ).result; + const paymentDataByAddress = cvToValue(paymentDataByAddressCV).value; + const createdAtCV: UIntCV = { + type: 1, + value: paymentDataByAddress.createdAt.value, + }; + const createdAt = cvToValue(createdAtCV); + + // Assert + const expectedInvoiceData = Cl.tuple({ + amount: Cl.uint(resourcePrice), + userIndex: Cl.uint(1), + resourceIndex: Cl.uint(1), + resourceName: Cl.stringUtf8(resourceName), + createdAt: Cl.uint(createdAt), + }); + + expect(paymentDataByAddressCV).toStrictEqual(Cl.some(expectedInvoiceData)); + }); +}); diff --git a/tests/dao/extensions/aibtc-payment-processor-stx.test.ts b/tests/dao/extensions/aibtc-payment-processor-stx.test.ts new file mode 100644 index 00000000..7f98bdba --- /dev/null +++ b/tests/dao/extensions/aibtc-payment-processor-stx.test.ts @@ -0,0 +1,992 @@ +import { Cl, cvToValue, ClarityValue, UIntCV } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { PaymentProcessorErrCode } from "../../error-codes"; +import { ContractType, ContractProposalType } from "../../dao-types"; +import { + constructDao, + dbgLog, + fundVoters, + passCoreProposal, + VOTING_CONFIG, +} from "../../test-utilities"; + +// Contract names for reuse +const accounts = simnet.getAccounts(); +const address1 = accounts.get("wallet_1")!; +const address2 = accounts.get("wallet_2")!; +const deployer = accounts.get("deployer")!; + +const contractAddress = `${deployer}.${ContractType.DAO_PAYMENT_PROCESSOR_STX}`; +const tokenContractAddress = `${deployer}.${ContractType.DAO_TOKEN}`; +const tokenDexContractAddress = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; +const baseDaoContractAddress = `${deployer}.${ContractType.DAO_BASE}`; +const bootstrapContractAddress = `${deployer}.${ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2}`; +const coreProposalsContractAddress = `${deployer}.${ContractType.DAO_CORE_PROPOSALS_V2}`; +const proposalContractAddress = `${deployer}.${ContractProposalType.DAO_PMT_STX_ADD_RESOURCE}`; + +const ErrCode = PaymentProcessorErrCode; + +// Test resource data +const resourceName = "example-resource"; +const resourceDescription = "An example resource"; +const resourcePrice = 1000000; // 1 STX +const resourceUrl = "https://example.com"; + +// Helper function to set up a test with a resource and optionally a user +function setupTest(createUser = false) { + // Setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_CORE_PROPOSALS_V2]; + + // Fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // Construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // Pass proposal to add resource + const concludeProposalReceipt = passCoreProposal( + coreProposalsContractAddress, + proposalContractAddress, + deployer, + [deployer, address1, address2], + votingConfig + ); + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + + if (createUser) { + // Pay an invoice + const payInvoice = simnet.callPublicFn( + contractAddress, + "pay-invoice", + [Cl.uint(1), Cl.none()], + address1 + ); + expect(payInvoice.result).toBeOk(Cl.uint(1)); + } +} + +describe(`public functions: ${ContractType.DAO_PAYMENT_PROCESSOR_STX}`, () => { + //////////////////////////////////////// + // callback() tests + //////////////////////////////////////// + it("callback() should respond with (ok true)", () => { + const callback = simnet.callPublicFn( + contractAddress, + "callback", + [Cl.principal(deployer), Cl.bufferFromAscii("test")], + deployer + ); + expect(callback.result).toBeOk(Cl.bool(true)); + }); + + //////////////////////////////////////// + // set-payment-address() tests + //////////////////////////////////////// + it("set-payment-address() fails if called directly", () => { + const setPaymentAddress = simnet.callPublicFn( + contractAddress, + "set-payment-address", + [Cl.principal(address1)], + deployer + ); + expect(setPaymentAddress.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION) + ); + }); + + //////////////////////////////////////// + // add-resource() tests + //////////////////////////////////////// + it("add-resource() fails if called directly", () => { + const addResource = simnet.callPublicFn( + contractAddress, + "add-resource", + [ + Cl.stringUtf8(resourceName), + Cl.stringUtf8(resourceDescription), + Cl.uint(resourcePrice), + Cl.some(Cl.stringUtf8(resourceUrl)), + ], + deployer + ); + expect(addResource.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION) + ); + }); + + //////////////////////////////////////// + // toggle-resource() tests + //////////////////////////////////////// + it("toggle-resource() fails if called directly", () => { + // NOTE: full check would pass and add one first + const toggleResource = simnet.callPublicFn( + contractAddress, + "toggle-resource", + [Cl.uint(1)], + deployer + ); + expect(toggleResource.result).toBeErr( + Cl.uint(ErrCode.ERR_RESOURCE_NOT_FOUND) + ); + }); + + //////////////////////////////////////// + // toggle-resource-by-name() tests + //////////////////////////////////////// + it("toggle-resource-by-name() fails if called directly", () => { + // NOTE: full check would pass and add one first + const toggleResourceByName = simnet.callPublicFn( + contractAddress, + "toggle-resource-by-name", + [Cl.stringUtf8(resourceName)], + deployer + ); + expect(toggleResourceByName.result).toBeErr( + Cl.uint(ErrCode.ERR_RESOURCE_NOT_FOUND) + ); + }); + + //////////////////////////////////////// + // pay-invoice() tests + //////////////////////////////////////// + it("pay-invoice() fails if resource is not found", () => { + const payInvoice = simnet.callPublicFn( + contractAddress, + "pay-invoice", + [Cl.uint(1), Cl.none()], + address1 + ); + expect(payInvoice.result).toBeErr(Cl.uint(ErrCode.ERR_RESOURCE_NOT_FOUND)); + }); + + it("pay-invoice() fails if resource index is 0", () => { + const payInvoice = simnet.callPublicFn( + contractAddress, + "pay-invoice", + [Cl.uint(0), Cl.none()], + address1 + ); + expect(payInvoice.result).toBeErr(Cl.uint(ErrCode.ERR_RESOURCE_NOT_FOUND)); + }); + + //////////////////////////////////////// + // pay-invoice-by-resource-name() tests + //////////////////////////////////////// + it("pay-invoice-by-resource-name() fails if resource is not found", () => { + const payInvoiceByName = simnet.callPublicFn( + contractAddress, + "pay-invoice-by-resource-name", + [Cl.stringUtf8(resourceName), Cl.none()], + address1 + ); + expect(payInvoiceByName.result).toBeErr( + Cl.uint(ErrCode.ERR_RESOURCE_NOT_FOUND) + ); + }); +}); + +describe(`read-only functions: ${ContractType.DAO_PAYMENT_PROCESSOR_STX}`, () => { + ///////////////////////////////////////////// + // get-total-users() tests + ///////////////////////////////////////////// + it("get-total-users() returns the total number of users before any are created", () => { + const getTotalUsers = simnet.callReadOnlyFn( + contractAddress, + "get-total-users", + [], + deployer + ).result; + expect(getTotalUsers).toStrictEqual(Cl.uint(0)); + }); + + it("get-total-users() returns the correct count after a user is created", () => { + // Arrange + // Setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_CORE_PROPOSALS_V2]; + + // Fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // Construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // Pass proposal to add resource + const concludeProposalReceipt = passCoreProposal( + coreProposalsContractAddress, + proposalContractAddress, + deployer, + [deployer, address1, address2], + votingConfig + ); + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + + // Pay an invoice + const payInvoice = simnet.callPublicFn( + contractAddress, + "pay-invoice", + [Cl.uint(1), Cl.none()], + address1 + ); + expect(payInvoice.result).toBeOk(Cl.uint(1)); + + // Act + const getTotalUsers = simnet.callReadOnlyFn( + contractAddress, + "get-total-users", + [], + deployer + ).result; + + // Assert + expect(getTotalUsers).toStrictEqual(Cl.uint(1)); + }); + + ///////////////////////////////////////////// + // get-total-resources() tests + ///////////////////////////////////////////// + it("get-total-resources() returns the total number of resources before any are created", () => { + const getTotalResources = simnet.callReadOnlyFn( + contractAddress, + "get-total-resources", + [], + deployer + ).result; + expect(getTotalResources).toStrictEqual(Cl.uint(0)); + }); + + it("get-total-resources() returns the correct count after a resource is added", () => { + // Arrange + // Setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_CORE_PROPOSALS_V2]; + + // Fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // Construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // Pass proposal to add resource + const concludeProposalReceipt = passCoreProposal( + coreProposalsContractAddress, + proposalContractAddress, + deployer, + [deployer, address1, address2], + votingConfig + ); + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + + // Act + const getTotalResources = simnet.callReadOnlyFn( + contractAddress, + "get-total-resources", + [], + deployer + ).result; + + // Assert + expect(getTotalResources).toStrictEqual(Cl.uint(1)); + }); + + ///////////////////////////////////////////// + // get-total-invoices() tests + ///////////////////////////////////////////// + it("get-total-invoices() returns the total number of invoices before any are created", () => { + const getTotalInvoices = simnet.callReadOnlyFn( + contractAddress, + "get-total-invoices", + [], + deployer + ).result; + expect(getTotalInvoices).toStrictEqual(Cl.uint(0)); + }); + + it("get-total-invoices() returns the correct count after an invoice is created", () => { + // Arrange + // Setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_CORE_PROPOSALS_V2]; + + // Fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // Construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // Pass proposal to add resource + const concludeProposalReceipt = passCoreProposal( + coreProposalsContractAddress, + proposalContractAddress, + deployer, + [deployer, address1, address2], + votingConfig + ); + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + + // Pay an invoice + const payInvoice = simnet.callPublicFn( + contractAddress, + "pay-invoice", + [Cl.uint(1), Cl.none()], + address1 + ); + expect(payInvoice.result).toBeOk(Cl.uint(1)); + + // Act + const getTotalInvoices = simnet.callReadOnlyFn( + contractAddress, + "get-total-invoices", + [], + deployer + ).result; + + // Assert + expect(getTotalInvoices).toStrictEqual(Cl.uint(1)); + }); + + ///////////////////////////////////////////// + // get-payment-address() tests + ///////////////////////////////////////////// + it("get-payment-address() returns the payment address", () => { + const getPaymentAddress = simnet.callReadOnlyFn( + contractAddress, + "get-payment-address", + [], + deployer + ).result; + // Default payment address should be the treasury + expect(getPaymentAddress).toStrictEqual( + Cl.some(Cl.principal(`${deployer}.aibtc-treasury`)) + ); + }); + + ///////////////////////////////////////////// + // get-total-revenue() tests + ///////////////////////////////////////////// + it("get-total-revenue() returns zero before any payments", () => { + const getTotalRevenue = simnet.callReadOnlyFn( + contractAddress, + "get-total-revenue", + [], + deployer + ).result; + expect(getTotalRevenue).toStrictEqual(Cl.uint(0)); + }); + + it("get-total-revenue() returns the correct total after payment", () => { + // Arrange + // Setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_CORE_PROPOSALS_V2]; + + // Fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // Construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // Pass proposal to add resource + const concludeProposalReceipt = passCoreProposal( + coreProposalsContractAddress, + proposalContractAddress, + deployer, + [deployer, address1, address2], + votingConfig + ); + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + + // Pay an invoice + const payInvoice = simnet.callPublicFn( + contractAddress, + "pay-invoice", + [Cl.uint(1), Cl.none()], + address1 + ); + expect(payInvoice.result).toBeOk(Cl.uint(1)); + + // Act + const getTotalRevenue = simnet.callReadOnlyFn( + contractAddress, + "get-total-revenue", + [], + deployer + ).result; + + // Assert + expect(getTotalRevenue).toStrictEqual(Cl.uint(resourcePrice)); + }); + + ///////////////////////////////////////////// + // get-contract-data() tests + ///////////////////////////////////////////// + it("get-contract-data() returns the initial contract data", () => { + const getContractData = simnet.callReadOnlyFn( + contractAddress, + "get-contract-data", + [], + deployer + ).result; + + const expectedData = Cl.tuple({ + contractAddress: Cl.principal(contractAddress), + paymentAddress: Cl.some(Cl.principal(`${deployer}.aibtc-treasury`)), + paymentToken: Cl.stringAscii("STX"), + totalInvoices: Cl.uint(0), + totalResources: Cl.uint(0), + totalRevenue: Cl.uint(0), + totalUsers: Cl.uint(0), + }); + + expect(getContractData).toStrictEqual(expectedData); + }); + + it("get-contract-data() returns updated contract data after payment", () => { + // Arrange + // Setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_CORE_PROPOSALS_V2]; + + // Fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // Construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // Pass proposal to add resource + const concludeProposalReceipt = passCoreProposal( + coreProposalsContractAddress, + proposalContractAddress, + deployer, + [deployer, address1, address2], + votingConfig + ); + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + + // Pay an invoice + const payInvoice = simnet.callPublicFn( + contractAddress, + "pay-invoice", + [Cl.uint(1), Cl.none()], + address1 + ); + expect(payInvoice.result).toBeOk(Cl.uint(1)); + + // Act + const getContractData = simnet.callReadOnlyFn( + contractAddress, + "get-contract-data", + [], + deployer + ).result; + + // Assert + const expectedData = Cl.tuple({ + contractAddress: Cl.principal(contractAddress), + paymentAddress: Cl.some(Cl.principal(`${deployer}.aibtc-treasury`)), + paymentToken: Cl.stringAscii("STX"), + totalInvoices: Cl.uint(1), + totalResources: Cl.uint(1), + totalRevenue: Cl.uint(resourcePrice), + totalUsers: Cl.uint(1), + }); + + expect(getContractData).toStrictEqual(expectedData); + }); + + ///////////////////////////////////////////// + // get-user-index() tests + ///////////////////////////////////////////// + it("get-user-index() returns none for non-existent user", () => { + const getUserIndex = simnet.callReadOnlyFn( + contractAddress, + "get-user-index", + [Cl.principal(address2)], + deployer + ).result; + expect(getUserIndex).toBeNone(); + }); + + it("get-user-index() returns the correct index for existing user", () => { + // Arrange + // Setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_CORE_PROPOSALS_V2]; + + // Fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // Construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // Pass proposal to add resource + const concludeProposalReceipt = passCoreProposal( + coreProposalsContractAddress, + proposalContractAddress, + deployer, + [deployer, address1, address2], + votingConfig + ); + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + + // Pay an invoice + const payInvoice = simnet.callPublicFn( + contractAddress, + "pay-invoice", + [Cl.uint(1), Cl.none()], + address1 + ); + expect(payInvoice.result).toBeOk(Cl.uint(1)); + + // Act + const getUserIndex = simnet.callReadOnlyFn( + contractAddress, + "get-user-index", + [Cl.principal(address1)], + deployer + ).result; + + // Assert + expect(getUserIndex).toStrictEqual(Cl.some(Cl.uint(1))); + }); + + ///////////////////////////////////////////// + // get-user-data() tests + ///////////////////////////////////////////// + it("get-user-data() returns none for non-existent user index", () => { + const getUserData = simnet.callReadOnlyFn( + contractAddress, + "get-user-data", + [Cl.uint(999)], + deployer + ).result; + expect(getUserData).toBeNone(); + }); + + it("get-user-data() returns the correct data for existing user", () => { + // Arrange + // Setup voting config + const votingConfig = VOTING_CONFIG[ContractType.DAO_CORE_PROPOSALS_V2]; + + // Fund accounts for creating and voting on proposals + fundVoters(tokenContractAddress, tokenDexContractAddress, [ + deployer, + address1, + address2, + ]); + + // Construct DAO + const constructReceipt = constructDao( + deployer, + baseDaoContractAddress, + bootstrapContractAddress + ); + expect(constructReceipt.result).toBeOk(Cl.bool(true)); + + // Pass proposal to add resource + const concludeProposalReceipt = passCoreProposal( + coreProposalsContractAddress, + proposalContractAddress, + deployer, + [deployer, address1, address2], + votingConfig + ); + expect(concludeProposalReceipt.result).toBeOk(Cl.bool(true)); + + // Pay an invoice + const payInvoice = simnet.callPublicFn( + contractAddress, + "pay-invoice", + [Cl.uint(1), Cl.none()], + address1 + ); + expect(payInvoice.result).toBeOk(Cl.uint(1)); + + // Act + const getUserData = simnet.callReadOnlyFn( + contractAddress, + "get-user-data", + [Cl.uint(1)], + deployer + ).result; + + // Assert + const expectedUserData = Cl.tuple({ + address: Cl.principal(address1), + totalSpent: Cl.uint(resourcePrice), + totalUsed: Cl.uint(1), + }); + + expect(getUserData).toStrictEqual(Cl.some(expectedUserData)); + }); + + ///////////////////////////////////////////// + // get-user-data-by-address() tests + ///////////////////////////////////////////// + it("get-user-data-by-address() returns none for non-existent user", () => { + const getUserDataByAddress = simnet.callReadOnlyFn( + contractAddress, + "get-user-data-by-address", + [Cl.principal(address2)], + deployer + ).result; + expect(getUserDataByAddress).toBeNone(); + }); + + it("get-user-data-by-address() returns the correct data for existing user", () => { + // Arrange + setupTest(true); + + // Act + const getUserDataByAddress = simnet.callReadOnlyFn( + contractAddress, + "get-user-data-by-address", + [Cl.principal(address1)], + deployer + ).result; + + // Assert + const expectedUserData = Cl.tuple({ + address: Cl.principal(address1), + totalSpent: Cl.uint(resourcePrice), + totalUsed: Cl.uint(1), + }); + + expect(getUserDataByAddress).toStrictEqual(Cl.some(expectedUserData)); + }); + + ///////////////////////////////////////////// + // get-resource-index() tests + ///////////////////////////////////////////// + it("get-resource-index() returns none for non-existent resource", () => { + const getResourceIndex = simnet.callReadOnlyFn( + contractAddress, + "get-resource-index", + [Cl.stringUtf8("non-existent-resource")], + deployer + ).result; + expect(getResourceIndex).toBeNone(); + }); + + it("get-resource-index() returns the correct index for existing resource", () => { + // Arrange + setupTest(false); + + // Act + const getResourceIndex = simnet.callReadOnlyFn( + contractAddress, + "get-resource-index", + [Cl.stringUtf8(resourceName)], + deployer + ).result; + + // Assert + expect(getResourceIndex).toStrictEqual(Cl.some(Cl.uint(1))); + }); + + ///////////////////////////////////////////// + // get-resource() tests + ///////////////////////////////////////////// + it("get-resource() returns none for non-existent resource index", () => { + const getResource = simnet.callReadOnlyFn( + contractAddress, + "get-resource", + [Cl.uint(999)], + deployer + ).result; + expect(getResource).toBeNone(); + }); + + it("get-resource() returns the correct data for existing resource", () => { + // Arrange + setupTest(false); + + // Act + const resourceRecordCV = simnet.callReadOnlyFn( + contractAddress, + "get-resource", + [Cl.uint(1)], + deployer + ).result; + const resourceRecord = cvToValue(resourceRecordCV).value; + const createdAtCV: UIntCV = { + type: 1, + value: resourceRecord.createdAt.value, + }; + const createdAt = cvToValue(createdAtCV); + // Assert + const expectedResourceData = Cl.tuple({ + name: Cl.stringUtf8(resourceName), + description: Cl.stringUtf8(resourceDescription), + price: Cl.uint(resourcePrice), + enabled: Cl.bool(true), + url: Cl.some(Cl.stringUtf8(resourceUrl)), + createdAt: Cl.uint(createdAt), + totalSpent: Cl.uint(0), + totalUsed: Cl.uint(0), + }); + + expect(resourceRecordCV).toStrictEqual(Cl.some(expectedResourceData)); + }); + + ///////////////////////////////////////////// + // get-resource-by-name() tests + ///////////////////////////////////////////// + it("get-resource-by-name() returns none for non-existent resource", () => { + const getResourceByName = simnet.callReadOnlyFn( + contractAddress, + "get-resource-by-name", + [Cl.stringUtf8("non-existent-resource")], + deployer + ).result; + expect(getResourceByName).toBeNone(); + }); + + it("get-resource-by-name() returns the correct data for existing resource", () => { + // Arrange + setupTest(false); + + // Act + const resourceRecordCV = simnet.callReadOnlyFn( + contractAddress, + "get-resource-by-name", + [Cl.stringUtf8(resourceName)], + deployer + ).result; + const resourceRecord = cvToValue(resourceRecordCV).value; + const createdAtCV: UIntCV = { + type: 1, + value: resourceRecord.createdAt.value, + }; + const createdAt = cvToValue(createdAtCV); + + // Assert + const expectedResourceData = Cl.tuple({ + name: Cl.stringUtf8(resourceName), + description: Cl.stringUtf8(resourceDescription), + price: Cl.uint(resourcePrice), + enabled: Cl.bool(true), + url: Cl.some(Cl.stringUtf8(resourceUrl)), + createdAt: Cl.uint(createdAt), + totalSpent: Cl.uint(0), + totalUsed: Cl.uint(0), + }); + + expect(resourceRecordCV).toStrictEqual(Cl.some(expectedResourceData)); + }); + + ///////////////////////////////////////////// + // get-invoice() tests + ///////////////////////////////////////////// + it("get-invoice() returns none for non-existent invoice index", () => { + const getInvoice = simnet.callReadOnlyFn( + contractAddress, + "get-invoice", + [Cl.uint(999)], + deployer + ).result; + expect(getInvoice).toBeNone(); + }); + + it("get-invoice() returns the correct data for existing invoice", () => { + // Arrange + setupTest(true); + + // Act + const invoiceRecordCV = simnet.callReadOnlyFn( + contractAddress, + "get-invoice", + [Cl.uint(1)], + deployer + ).result; + const invoiceRecord = cvToValue(invoiceRecordCV).value; + const createdAtCV: UIntCV = { + type: 1, + value: invoiceRecord.createdAt.value, + }; + const createdAt = cvToValue(createdAtCV); + + // Assert + const expectedInvoiceData = Cl.tuple({ + amount: Cl.uint(resourcePrice), + userIndex: Cl.uint(1), + resourceIndex: Cl.uint(1), + resourceName: Cl.stringUtf8(resourceName), + createdAt: Cl.uint(createdAt), + }); + + expect(invoiceRecordCV).toStrictEqual(Cl.some(expectedInvoiceData)); + }); + + ///////////////////////////////////////////// + // get-recent-payment() tests + ///////////////////////////////////////////// + it("get-recent-payment() returns none for non-existent user/resource combination", () => { + const getRecentPayment = simnet.callReadOnlyFn( + contractAddress, + "get-recent-payment", + [Cl.uint(999), Cl.uint(999)], + deployer + ).result; + expect(getRecentPayment).toBeNone(); + }); + + it("get-recent-payment() returns the correct invoice index for existing user/resource", () => { + // Arrange + setupTest(true); + + // Act + const getRecentPayment = simnet.callReadOnlyFn( + contractAddress, + "get-recent-payment", + [Cl.uint(1), Cl.uint(1)], + deployer + ).result; + + // Assert + expect(getRecentPayment).toStrictEqual(Cl.some(Cl.uint(1))); + }); + + ///////////////////////////////////////////// + // get-recent-payment-data() tests + ///////////////////////////////////////////// + it("get-recent-payment-data() returns none for non-existent user/resource combination", () => { + const getRecentPaymentData = simnet.callReadOnlyFn( + contractAddress, + "get-recent-payment-data", + [Cl.uint(999), Cl.uint(999)], + deployer + ).result; + expect(getRecentPaymentData).toBeNone(); + }); + + it("get-recent-payment-data() returns the correct data for existing user/resource", () => { + // Arrange + setupTest(true); + + // Act + const paymentDataCV = simnet.callReadOnlyFn( + contractAddress, + "get-recent-payment-data", + [Cl.uint(1), Cl.uint(1)], + deployer + ).result; + const paymentData = cvToValue(paymentDataCV).value; + const createdAtCV: UIntCV = { + type: 1, + value: paymentData.createdAt.value, + }; + const createdAt = cvToValue(createdAtCV); + + // Assert + const expectedInvoiceData = Cl.tuple({ + amount: Cl.uint(resourcePrice), + userIndex: Cl.uint(1), + resourceIndex: Cl.uint(1), + resourceName: Cl.stringUtf8(resourceName), + createdAt: Cl.uint(createdAt), + }); + + expect(paymentDataCV).toStrictEqual(Cl.some(expectedInvoiceData)); + }); + + ///////////////////////////////////////////// + // get-recent-payment-data-by-address() tests + ///////////////////////////////////////////// + it("get-recent-payment-data-by-address() returns none for non-existent user/resource combination", () => { + const getRecentPaymentDataByAddress = simnet.callReadOnlyFn( + contractAddress, + "get-recent-payment-data-by-address", + [Cl.stringUtf8("non-existent-resource"), Cl.principal(address2)], + deployer + ).result; + expect(getRecentPaymentDataByAddress).toBeNone(); + }); + + it("get-recent-payment-data-by-address() returns the correct data for existing user/resource", () => { + // Arrange + setupTest(true); + + // Act + const paymentDataByAddressCV = simnet.callReadOnlyFn( + contractAddress, + "get-recent-payment-data-by-address", + [Cl.stringUtf8(resourceName), Cl.principal(address1)], + deployer + ).result; + const paymentDataByAddress = cvToValue(paymentDataByAddressCV).value; + const createdAtCV: UIntCV = { + type: 1, + value: paymentDataByAddress.createdAt.value, + }; + const createdAt = cvToValue(createdAtCV); + + // Assert + const expectedInvoiceData = Cl.tuple({ + amount: Cl.uint(resourcePrice), + userIndex: Cl.uint(1), + resourceIndex: Cl.uint(1), + resourceName: Cl.stringUtf8(resourceName), + createdAt: Cl.uint(createdAt), + }); + + expect(paymentDataByAddressCV).toStrictEqual(Cl.some(expectedInvoiceData)); + }); +}); diff --git a/tests/dao/extensions/aibtc-payments-invoices.test.ts b/tests/dao/extensions/aibtc-payments-invoices.test.ts deleted file mode 100644 index 01be3c56..00000000 --- a/tests/dao/extensions/aibtc-payments-invoices.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Cl } from "@stacks/transactions"; -import { describe, expect, it } from "vitest"; -import { PaymentsInvoicesErrCode } from "../../error-codes"; - -const accounts = simnet.getAccounts(); -const address1 = accounts.get("wallet_1")!; -const address2 = accounts.get("wallet_2")!; -const deployer = accounts.get("deployer")!; - -const contractName = "aibtc-payments-invoices"; -const contractAddress = `${deployer}.${contractName}`; - -const ErrCode = PaymentsInvoicesErrCode; - -describe(`extension: ${contractName}`, () => { - it("callback() should respond with (ok true)", () => { - const callback = simnet.callPublicFn( - contractAddress, - "callback", - [Cl.principal(deployer), Cl.bufferFromAscii("test")], - deployer - ); - expect(callback.result).toBeOk(Cl.bool(true)); - }); - /* - // 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-timed-vault-dao.test.ts b/tests/dao/extensions/aibtc-timed-vault-dao.test.ts new file mode 100644 index 00000000..1f3b395a --- /dev/null +++ b/tests/dao/extensions/aibtc-timed-vault-dao.test.ts @@ -0,0 +1,215 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { TimedVaultErrCode } from "../../error-codes"; +import { ContractType } from "../../dao-types"; +import { getDaoTokens } from "../../test-utilities"; + +const accounts = simnet.getAccounts(); +const address1 = accounts.get("wallet_1")!; +const address2 = accounts.get("wallet_2")!; +const deployer = accounts.get("deployer")!; + +const contractAddress = `${deployer}.${ContractType.DAO_TIMED_VAULT_DAO}`; + +const ErrCode = TimedVaultErrCode; + +const withdrawalAmount = 100000000000; // 1,000 DAO tokens (8 decimals) +const withdrawalPeriod = 144; // 144 blocks + +describe(`public functions: ${ContractType.DAO_TIMED_VAULT_DAO}`, () => { + //////////////////////////////////////// + // callback() tests + //////////////////////////////////////// + it("callback() should respond with (ok true)", () => { + const callback = simnet.callPublicFn( + contractAddress, + "callback", + [Cl.principal(deployer), Cl.bufferFromAscii("test")], + deployer + ); + expect(callback.result).toBeOk(Cl.bool(true)); + }); + //////////////////////////////////////// + // set-account-holder() tests + //////////////////////////////////////// + it("set-account-holder() fails if called directly", () => { + const setAccountHolder = simnet.callPublicFn( + contractAddress, + "set-account-holder", + [Cl.principal(address1)], + deployer + ); + expect(setAccountHolder.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION) + ); + }); + /////////////////////////////////////////// + // set-withdrawal-period() tests + /////////////////////////////////////////// + it("set-withdrawal-period() fails if called directly", () => { + const setWithdrawalPeriod = simnet.callPublicFn( + contractAddress, + "set-withdrawal-period", + [Cl.uint(withdrawalPeriod)], + deployer + ); + expect(setWithdrawalPeriod.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION) + ); + }); + /////////////////////////////////////////// + // set-withdrawal-amount() tests + /////////////////////////////////////////// + it("set-withdrawal-amount() fails if called directly", () => { + const setWithdrawalAmount = simnet.callPublicFn( + contractAddress, + "set-withdrawal-amount", + [Cl.uint(withdrawalAmount)], + deployer + ); + expect(setWithdrawalAmount.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION) + ); + }); + /////////////////////////////////////////// + // override-last-withdrawal-block() tests + /////////////////////////////////////////// + it("override-last-withdrawal-block() fails if called directly", () => { + const overrideLastWithdrawalBlock = simnet.callPublicFn( + contractAddress, + "override-last-withdrawal-block", + [Cl.uint(100)], + deployer + ); + expect(overrideLastWithdrawalBlock.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION) + ); + }); + /////////////////////////////////////////// + // deposit() tests + /////////////////////////////////////////// + it("deposit() fails if amount is 0", () => { + const depositDao = simnet.callPublicFn( + contractAddress, + "deposit", + [Cl.uint(0)], + deployer + ); + expect(depositDao.result).toBeErr(Cl.uint(ErrCode.ERR_INVALID_AMOUNT)); + }); + it("deposit() succeeds and transfers DAO tokens to contract", () => { + // arrange + const daoTokenContract = `${deployer}.${ContractType.DAO_TOKEN}`; + const daoTokenDexContract = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const satsAmount = 400000; // 400,000 sats or 0.004 BTC + const depositAmount = Cl.uint(withdrawalAmount); + const faucetReceipt = getDaoTokens( + daoTokenContract, + daoTokenDexContract, + deployer, + satsAmount + ); + expect(faucetReceipt.result).toBeOk(Cl.bool(true)); + const depositDaoReceipt = simnet.callPublicFn( + contractAddress, + "deposit", + [depositAmount], + deployer + ); + expect(depositDaoReceipt.result).toBeOk(Cl.bool(true)); + const depositDao = simnet.callPublicFn( + contractAddress, + "deposit", + [Cl.uint(withdrawalAmount)], + deployer + ); + expect(depositDao.result).toBeOk(Cl.bool(true)); + }); + /////////////////////////////////////////// + // withdraw() tests + /////////////////////////////////////////// + it("withdraw() fails if caller is not account holder", () => { + const withdrawDao = simnet.callPublicFn( + contractAddress, + "withdraw", + [], + address2 + ); + expect(withdrawDao.result).toBeErr(Cl.uint(ErrCode.ERR_NOT_ACCOUNT_HOLDER)); + }); +}); + +describe(`read-only functions: ${ContractType.DAO_TIMED_VAULT_DAO}`, () => { + ///////////////////////////////////////////// + // get-account-balance() tests + ///////////////////////////////////////////// + it("get-account-balance() returns the contract account balance", () => { + // arrange + const expectedResult = Cl.ok(Cl.uint(0)); + // act + const getAccountBalance = simnet.callReadOnlyFn( + contractAddress, + "get-account-balance", + [], + deployer + ).result; + // assert + expect(getAccountBalance).toStrictEqual(expectedResult); + // arrange + const daoTokenContract = `${deployer}.${ContractType.DAO_TOKEN}`; + const daoTokenDexContract = `${deployer}.${ContractType.DAO_TOKEN_DEX}`; + const satsAmount = 400000; // 400,000 sats or 0.004 BTC + const depositAmount = Cl.uint(10000000000); // 100 dao token, 8 decimals + const faucetReceipt = getDaoTokens( + daoTokenContract, + daoTokenDexContract, + deployer, + satsAmount + ); + expect(faucetReceipt.result).toBeOk(Cl.bool(true)); + const depositDaoReceipt = simnet.callPublicFn( + contractAddress, + "deposit", + [depositAmount], + deployer + ); + expect(depositDaoReceipt.result).toBeOk(Cl.bool(true)); + const expectedResult2 = Cl.ok(depositAmount); + // act + const getAccountBalance2 = simnet.callReadOnlyFn( + contractAddress, + "get-account-balance", + [], + deployer + ).result; + // assert + expect(getAccountBalance2).toStrictEqual(expectedResult2); + }); + + ///////////////////////////////////////////// + // get-account-terms() tests + ///////////////////////////////////////////// + it("get-account-terms() returns the contract account terms", () => { + // arrange + const daoTokenContract = `${deployer}.${ContractType.DAO_TOKEN}`; + const expectedResult = Cl.tuple({ + accountHolder: Cl.principal(contractAddress), + contractName: Cl.principal(contractAddress), + deployedBurnBlock: Cl.uint(5), + deployedStacksBlock: Cl.uint(6), + lastWithdrawalBlock: Cl.uint(0), + vaultToken: Cl.principal(daoTokenContract), + withdrawalAmount: Cl.uint(withdrawalAmount), + withdrawalPeriod: Cl.uint(withdrawalPeriod), + }); + // act + const getAccountTerms = simnet.callReadOnlyFn( + contractAddress, + "get-account-terms", + [], + deployer + ).result; + // assert + expect(getAccountTerms).toStrictEqual(expectedResult); + }); +}); diff --git a/tests/dao/extensions/aibtc-timed-vault-sbtc.test.ts b/tests/dao/extensions/aibtc-timed-vault-sbtc.test.ts new file mode 100644 index 00000000..f3905285 --- /dev/null +++ b/tests/dao/extensions/aibtc-timed-vault-sbtc.test.ts @@ -0,0 +1,211 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { TimedVaultErrCode } from "../../error-codes"; +import { ContractType } from "../../dao-types"; + +const accounts = simnet.getAccounts(); +const address1 = accounts.get("wallet_1")!; +const address2 = accounts.get("wallet_2")!; +const deployer = accounts.get("deployer")!; + +const contractAddress = `${deployer}.${ContractType.DAO_TIMED_VAULT_SBTC}`; + +const ErrCode = TimedVaultErrCode; + +const withdrawalAmount = 10000; // 0.0001 sBTC (8 decimals) +const withdrawalPeriod = 144; // 144 blocks + +describe(`public functions: ${ContractType.DAO_TIMED_VAULT_SBTC}`, () => { + //////////////////////////////////////// + // callback() tests + //////////////////////////////////////// + it("callback() should respond with (ok true)", () => { + const callback = simnet.callPublicFn( + contractAddress, + "callback", + [Cl.principal(deployer), Cl.bufferFromAscii("test")], + deployer + ); + expect(callback.result).toBeOk(Cl.bool(true)); + }); + //////////////////////////////////////// + // set-account-holder() tests + //////////////////////////////////////// + it("set-account-holder() fails if called directly", () => { + const setAccountHolder = simnet.callPublicFn( + contractAddress, + "set-account-holder", + [Cl.principal(address1)], + deployer + ); + expect(setAccountHolder.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION) + ); + }); + /////////////////////////////////////////// + // set-withdrawal-period() tests + /////////////////////////////////////////// + it("set-withdrawal-period() fails if called directly", () => { + const setWithdrawalPeriod = simnet.callPublicFn( + contractAddress, + "set-withdrawal-period", + [Cl.uint(withdrawalPeriod)], + deployer + ); + expect(setWithdrawalPeriod.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION) + ); + }); + /////////////////////////////////////////// + // set-withdrawal-amount() tests + /////////////////////////////////////////// + it("set-withdrawal-amount() fails if called directly", () => { + const setWithdrawalAmount = simnet.callPublicFn( + contractAddress, + "set-withdrawal-amount", + [Cl.uint(withdrawalAmount)], + deployer + ); + expect(setWithdrawalAmount.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION) + ); + }); + /////////////////////////////////////////// + // override-last-withdrawal-block() tests + /////////////////////////////////////////// + it("override-last-withdrawal-block() fails if called directly", () => { + const overrideLastWithdrawalBlock = simnet.callPublicFn( + contractAddress, + "override-last-withdrawal-block", + [Cl.uint(100)], + deployer + ); + expect(overrideLastWithdrawalBlock.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION) + ); + }); + /////////////////////////////////////////// + // deposit() tests + /////////////////////////////////////////// + it("deposit() fails if amount is 0", () => { + const depositSbtc = simnet.callPublicFn( + contractAddress, + "deposit", + [Cl.uint(0)], + deployer + ); + expect(depositSbtc.result).toBeErr(Cl.uint(ErrCode.ERR_INVALID_AMOUNT)); + }); + it("deposit() succeeds and transfers sBTC to contract", () => { + // Get sBTC from faucet first + const sbtcContract = "STV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RJ5XDY2.sbtc-token"; + const faucetReceipt = simnet.callPublicFn( + sbtcContract, + "faucet", + [], + deployer + ); + expect(faucetReceipt.result).toBeOk(Cl.bool(true)); + + // Now deposit sBTC to the vault + const depositSbtc = simnet.callPublicFn( + contractAddress, + "deposit", + [Cl.uint(withdrawalAmount)], + deployer + ); + expect(depositSbtc.result).toBeOk(Cl.bool(true)); + }); + /////////////////////////////////////////// + // withdraw() tests + /////////////////////////////////////////// + it("withdraw() fails if caller is not account holder", () => { + const withdrawSbtc = simnet.callPublicFn( + contractAddress, + "withdraw", + [], + address2 + ); + expect(withdrawSbtc.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_ACCOUNT_HOLDER) + ); + }); +}); + +describe(`read-only functions: ${ContractType.DAO_TIMED_VAULT_SBTC}`, () => { + ///////////////////////////////////////////// + // get-account-balance() tests + ///////////////////////////////////////////// + it("get-account-balance() returns the contract account balance", () => { + // arrange + const sbtcContract = "STV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RJ5XDY2.sbtc-token"; + const expectedResult = Cl.ok(Cl.uint(0)); + + // act - check initial balance + const getAccountBalance = simnet.callReadOnlyFn( + contractAddress, + "get-account-balance", + [], + deployer + ).result; + + // assert - initial balance should be 0 + expect(getAccountBalance).toStrictEqual(expectedResult); + + // arrange - get sBTC from faucet and deposit + const faucetReceipt = simnet.callPublicFn( + sbtcContract, + "faucet", + [], + deployer + ); + expect(faucetReceipt.result).toBeOk(Cl.bool(true)); + + const depositAmount = Cl.uint(10000000); // 10 sBTC (in sats) + const depositSbtcReceipt = simnet.callPublicFn( + contractAddress, + "deposit", + [depositAmount], + deployer + ); + expect(depositSbtcReceipt.result).toBeOk(Cl.bool(true)); + + // act - check balance after deposit + const getAccountBalance2 = simnet.callReadOnlyFn( + contractAddress, + "get-account-balance", + [], + deployer + ).result; + + // assert - balance should match deposit + expect(getAccountBalance2).toBeOk(depositAmount); + }); + + ///////////////////////////////////////////// + // get-account-terms() tests + ///////////////////////////////////////////// + it("get-account-terms() returns the contract account terms", () => { + // arrange + const sbtcContract = "STV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RJ5XDY2.sbtc-token"; + const expectedResult = Cl.tuple({ + accountHolder: Cl.principal(contractAddress), + contractName: Cl.principal(contractAddress), + deployedBurnBlock: Cl.uint(5), + deployedStacksBlock: Cl.uint(6), + lastWithdrawalBlock: Cl.uint(0), + vaultToken: Cl.principal(sbtcContract), + withdrawalAmount: Cl.uint(withdrawalAmount), + withdrawalPeriod: Cl.uint(withdrawalPeriod), + }); + // act + const getAccountTerms = simnet.callReadOnlyFn( + contractAddress, + "get-account-terms", + [], + deployer + ).result; + // assert + expect(getAccountTerms).toStrictEqual(expectedResult); + }); +}); diff --git a/tests/dao/extensions/aibtc-timed-vault-stx.test.ts b/tests/dao/extensions/aibtc-timed-vault-stx.test.ts new file mode 100644 index 00000000..3b5c1d05 --- /dev/null +++ b/tests/dao/extensions/aibtc-timed-vault-stx.test.ts @@ -0,0 +1,186 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { TimedVaultErrCode } from "../../error-codes"; +import { ContractType } from "../../dao-types"; + +const accounts = simnet.getAccounts(); +const address1 = accounts.get("wallet_1")!; +const address2 = accounts.get("wallet_2")!; +const deployer = accounts.get("deployer")!; + +const contractAddress = `${deployer}.${ContractType.DAO_TIMED_VAULT_STX}`; + +const ErrCode = TimedVaultErrCode; + +const withdrawalAmount = 10000000; // 10 STX (6 decimals) +const withdrawalPeriod = 144; // 144 blocks + +describe(`public functions: ${ContractType.DAO_TIMED_VAULT_STX}`, () => { + //////////////////////////////////////// + // callback() tests + //////////////////////////////////////// + it("callback() should respond with (ok true)", () => { + const callback = simnet.callPublicFn( + contractAddress, + "callback", + [Cl.principal(deployer), Cl.bufferFromAscii("test")], + deployer + ); + expect(callback.result).toBeOk(Cl.bool(true)); + }); + //////////////////////////////////////// + // set-account-holder() tests + //////////////////////////////////////// + it("set-account-holder() fails if called directly", () => { + const setAccountHolder = simnet.callPublicFn( + contractAddress, + "set-account-holder", + [Cl.principal(address1)], + deployer + ); + expect(setAccountHolder.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION) + ); + }); + /////////////////////////////////////////// + // set-withdrawal-period() tests + /////////////////////////////////////////// + it("set-withdrawal-period() fails if called directly", () => { + const setWithdrawalPeriod = simnet.callPublicFn( + contractAddress, + "set-withdrawal-period", + [Cl.uint(withdrawalPeriod)], + deployer + ); + expect(setWithdrawalPeriod.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION) + ); + }); + /////////////////////////////////////////// + // set-withdrawal-amount() tests + /////////////////////////////////////////// + it("set-withdrawal-amount() fails if called directly", () => { + const setWithdrawalAmount = simnet.callPublicFn( + contractAddress, + "set-withdrawal-amount", + [Cl.uint(withdrawalAmount)], + deployer + ); + expect(setWithdrawalAmount.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION) + ); + }); + /////////////////////////////////////////// + // override-last-withdrawal-block() tests + /////////////////////////////////////////// + it("override-last-withdrawal-block() fails if called directly", () => { + const overrideLastWithdrawalBlock = simnet.callPublicFn( + contractAddress, + "override-last-withdrawal-block", + [Cl.uint(100)], + deployer + ); + expect(overrideLastWithdrawalBlock.result).toBeErr( + Cl.uint(ErrCode.ERR_NOT_DAO_OR_EXTENSION) + ); + }); + /////////////////////////////////////////// + // deposit() tests + /////////////////////////////////////////// + it("deposit() fails if amount is 0", () => { + const depositStx = simnet.callPublicFn( + contractAddress, + "deposit", + [Cl.uint(0)], + deployer + ); + expect(depositStx.result).toBeErr(Cl.uint(ErrCode.ERR_INVALID_AMOUNT)); + }); + it("deposit() succeeds and transfers STX to contract", () => { + const depositStx = simnet.callPublicFn( + contractAddress, + "deposit", + [Cl.uint(withdrawalAmount)], + deployer + ); + expect(depositStx.result).toBeOk(Cl.bool(true)); + }); + /////////////////////////////////////////// + // withdraw() tests + /////////////////////////////////////////// + it("withdraw() fails if caller is not account holder", () => { + const withdrawStx = simnet.callPublicFn( + contractAddress, + "withdraw", + [], + address2 + ); + expect(withdrawStx.result).toBeErr(Cl.uint(ErrCode.ERR_NOT_ACCOUNT_HOLDER)); + }); +}); + +describe(`read-only functions: ${ContractType.DAO_TIMED_VAULT_STX}`, () => { + ///////////////////////////////////////////// + // get-account-balance() tests + ///////////////////////////////////////////// + it("get-account-balance() returns the contract account balance", () => { + // arrange + const expectedResult = Cl.ok(Cl.uint(0)); + // act + const getAccountBalance = simnet.callReadOnlyFn( + contractAddress, + "get-account-balance", + [], + deployer + ).result; + // assert + expect(getAccountBalance).toStrictEqual(expectedResult); + + // arrange - deposit some STX + const depositAmount = Cl.uint(10000000); // 10 STX + const depositStxReceipt = simnet.callPublicFn( + contractAddress, + "deposit", + [depositAmount], + deployer + ); + expect(depositStxReceipt.result).toBeOk(Cl.bool(true)); + + // act - check balance after deposit + const getAccountBalance2 = simnet.callReadOnlyFn( + contractAddress, + "get-account-balance", + [], + deployer + ).result; + + // assert - balance should match deposit + expect(getAccountBalance2).toBeOk(depositAmount); + }); + + ///////////////////////////////////////////// + // get-account-terms() tests + ///////////////////////////////////////////// + it("get-account-terms() returns the contract account terms", () => { + // arrange + const expectedResult = Cl.tuple({ + accountHolder: Cl.principal(contractAddress), + contractName: Cl.principal(contractAddress), + deployedBurnBlock: Cl.uint(5), + deployedStacksBlock: Cl.uint(6), + lastWithdrawalBlock: Cl.uint(0), + vaultToken: Cl.stringAscii("STX"), + withdrawalAmount: Cl.uint(withdrawalAmount), + withdrawalPeriod: Cl.uint(withdrawalPeriod), + }); + // act + const getAccountTerms = simnet.callReadOnlyFn( + contractAddress, + "get-account-terms", + [], + deployer + ).result; + // assert + expect(getAccountTerms).toStrictEqual(expectedResult); + }); +}); diff --git a/tests/dao/extensions/aibtc-timed-vault.test.ts b/tests/dao/extensions/aibtc-timed-vault.test.ts deleted file mode 100644 index 27cfaa96..00000000 --- a/tests/dao/extensions/aibtc-timed-vault.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Cl } from "@stacks/transactions"; -import { describe, expect, it } from "vitest"; -import { TimedVaultErrCode } from "../../error-codes"; - -const accounts = simnet.getAccounts(); -const address1 = accounts.get("wallet_1")!; -const address2 = accounts.get("wallet_2")!; -const deployer = accounts.get("deployer")!; - -const contractName = "aibtc-timed-vault"; -const contractAddress = `${deployer}.${contractName}`; - -const ErrCode = TimedVaultErrCode; - -const withdrawalAmount = 10000000; // 10 STX -const withdrawalPeriod = 144; // 144 blocks - -describe(`extension: ${contractName}`, () => { - it("callback() should respond with (ok true)", () => { - const callback = simnet.callPublicFn( - contractAddress, - "callback", - [Cl.principal(deployer), Cl.bufferFromAscii("test")], - deployer - ); - expect(callback.result).toBeOk(Cl.bool(true)); - }); - /* - // 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-treasury.test.ts b/tests/dao/extensions/aibtc-treasury.test.ts index cffe6177..9261d20d 100644 --- a/tests/dao/extensions/aibtc-treasury.test.ts +++ b/tests/dao/extensions/aibtc-treasury.test.ts @@ -1,5 +1,6 @@ import { Cl } from "@stacks/transactions"; import { describe, expect, it } from "vitest"; +import { ContractType } from "../../dao-types"; import { TreasuryErrCode } from "../../error-codes"; const accounts = simnet.getAccounts(); @@ -7,8 +8,9 @@ const address1 = accounts.get("wallet_1")!; const address2 = accounts.get("wallet_2")!; const deployer = accounts.get("deployer")!; -const contractName = "aibtc-treasury"; +const contractName = ContractType.DAO_TREASURY; const contractAddress = `${deployer}.${contractName}`; +const ftContractAddress = `${deployer}.sip010-token`; const ErrCode = TreasuryErrCode; @@ -22,70 +24,176 @@ describe(`extension: ${contractName}`, () => { ); expect(callback.result).toBeOk(Cl.bool(true)); }); - /* - // 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"); - }); + describe("public functions", () => { + it("allow-asset() fails if caller is not DAO or extension", () => { + // Arrange + const asset = address1; + const enabled = true; - // Deposit STX Tests - describe("deposit-stx()", () => { - it("succeeds and deposits STX to the treasury"); - }); + // Act + const receipt = simnet.callPublicFn( + contractAddress, + "allow-asset", + [Cl.principal(asset), Cl.bool(enabled)], + address2 // Unauthorized caller + ); - // Deposit FT Tests - describe("deposit-ft()", () => { - it("fails if asset is not allowed"); - it("succeeds and transfers FT to treasury"); - }); + // Assert + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_UNAUTHORIZED)); + }); - // Deposit NFT Tests - describe("deposit-nft()", () => { - it("fails if asset is not allowed"); - it("succeeds and transfers NFT to treasury"); - }); + it("allow-assets() fails if caller is not DAO or extension", () => { + // Arrange + const allowList = [ + { token: address1, enabled: true }, + { token: address2, enabled: false }, + ]; - // 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"); - }); + // Act + const receipt = simnet.callPublicFn( + contractAddress, + "allow-assets", + [ + Cl.list( + allowList.map((item) => + Cl.tuple({ + token: Cl.principal(item.token), + enabled: Cl.bool(item.enabled), + }) + ) + ), + ], + address2 // Unauthorized caller + ); - // 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"); - }); + // Assert + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_UNAUTHORIZED)); + }); - // 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"); - }); + it("withdraw-stx() fails if caller is not DAO or extension", () => { + // Arrange + const amount = 1000; + const recipient = address1; + + // Act + const receipt = simnet.callPublicFn( + contractAddress, + "withdraw-stx", + [Cl.uint(amount), Cl.principal(recipient)], + address2 // Unauthorized caller + ); + + // Assert + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_UNAUTHORIZED)); + }); + + it("withdraw-ft() fails if caller is not DAO or extension", () => { + // Arrange + const amount = 1000; + const recipient = address1; + + // Act + const receipt = simnet.callPublicFn( + contractAddress, + "withdraw-ft", + [ + Cl.contractPrincipal(deployer, "sip010-token"), + Cl.uint(amount), + Cl.principal(recipient), + ], + address2 // Unauthorized caller + ); + + // Assert + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_UNAUTHORIZED)); + }); - // Delegate STX Tests - describe("delegate-stx()", () => { - it("fails if caller is not DAO or extension"); - it("succeeds and delegates to Stacks PoX"); + it("withdraw-nft() fails if caller is not DAO or extension", () => { + // Arrange + const tokenId = 1; + const recipient = address1; + + // Act + const receipt = simnet.callPublicFn( + contractAddress, + "withdraw-nft", + [ + Cl.contractPrincipal(deployer, "nft-trait"), + Cl.uint(tokenId), + Cl.principal(recipient), + ], + address2 // Unauthorized caller + ); + + // Assert + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_UNAUTHORIZED)); + }); + + it("delegate-stx() fails if caller is not DAO or extension", () => { + // Arrange + const maxAmount = 1000; + const delegateTo = address1; + + // Act + const receipt = simnet.callPublicFn( + contractAddress, + "delegate-stx", + [Cl.uint(maxAmount), Cl.principal(delegateTo)], + address2 // Unauthorized caller + ); + + // Assert + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_UNAUTHORIZED)); + }); + + it("revoke-delegate-stx() fails if caller is not DAO or extension", () => { + // Arrange - No specific arrangement needed + + // Act + const receipt = simnet.callPublicFn( + contractAddress, + "revoke-delegate-stx", + [], + address2 // Unauthorized caller + ); + + // Assert + expect(receipt.result).toBeErr(Cl.uint(ErrCode.ERR_UNAUTHORIZED)); + }); }); - // 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"); + describe("read-only functions", () => { + it("is-allowed-asset() returns false for non-allowed assets", () => { + // Arrange + const asset = address1; + + // Act + const result = simnet.callReadOnlyFn( + contractAddress, + "is-allowed-asset", + [Cl.principal(asset)], + deployer + ); + + // Assert + expect(result.result).toStrictEqual(Cl.bool(false)); + }); + + it("get-allowed-asset() returns none for non-allowed assets", () => { + // Arrange + const asset = address1; + + // Act + const result = simnet.callReadOnlyFn( + contractAddress, + "get-allowed-asset", + [Cl.principal(asset)], + deployer + ); + + // Assert + expect(result.result).toStrictEqual(Cl.none()); + }); }); - */ }); diff --git a/tests/dao/proposals/aibtc-timed-vault-withdraw-stx.test.ts b/tests/dao/proposals/aibtc-pmt-dao-add-resource.test.ts similarity index 92% rename from tests/dao/proposals/aibtc-timed-vault-withdraw-stx.test.ts rename to tests/dao/proposals/aibtc-pmt-dao-add-resource.test.ts index 893138ca..f5715d35 100644 --- a/tests/dao/proposals/aibtc-timed-vault-withdraw-stx.test.ts +++ b/tests/dao/proposals/aibtc-pmt-dao-add-resource.test.ts @@ -4,7 +4,7 @@ import { OnchainMessagingErrCode } from "../../error-codes"; const accounts = simnet.getAccounts(); const deployer = accounts.get("deployer")!; -const contractName = "aibtc-timed-vault-withdraw-stx"; +const contractName = "aibtc-pmt-dao-add-resource"; const contractAddress = `${deployer}.${contractName}`; const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); diff --git a/tests/dao/proposals/aibtc-timed-vault-set-account-holder.test.ts b/tests/dao/proposals/aibtc-pmt-dao-set-payment-address.test.ts similarity index 91% rename from tests/dao/proposals/aibtc-timed-vault-set-account-holder.test.ts rename to tests/dao/proposals/aibtc-pmt-dao-set-payment-address.test.ts index c79e1853..42588907 100644 --- a/tests/dao/proposals/aibtc-timed-vault-set-account-holder.test.ts +++ b/tests/dao/proposals/aibtc-pmt-dao-set-payment-address.test.ts @@ -4,7 +4,7 @@ import { OnchainMessagingErrCode } from "../../error-codes"; const accounts = simnet.getAccounts(); const deployer = accounts.get("deployer")!; -const contractName = "aibtc-timed-vault-set-account-holder"; +const contractName = "aibtc-pmt-dao-set-payment-address"; const contractAddress = `${deployer}.${contractName}`; const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); diff --git a/tests/dao/proposals/aibtc-payments-invoices-toggle-resource.test.ts b/tests/dao/proposals/aibtc-pmt-dao-toggle-resource-by-name.test.ts similarity index 91% rename from tests/dao/proposals/aibtc-payments-invoices-toggle-resource.test.ts rename to tests/dao/proposals/aibtc-pmt-dao-toggle-resource-by-name.test.ts index 3d1808db..b38a3dc3 100644 --- a/tests/dao/proposals/aibtc-payments-invoices-toggle-resource.test.ts +++ b/tests/dao/proposals/aibtc-pmt-dao-toggle-resource-by-name.test.ts @@ -4,7 +4,7 @@ import { OnchainMessagingErrCode } from "../../error-codes"; const accounts = simnet.getAccounts(); const deployer = accounts.get("deployer")!; -const contractName = "aibtc-payments-invoices-toggle-resource"; +const contractName = "aibtc-pmt-dao-toggle-resource-by-name"; const contractAddress = `${deployer}.${contractName}`; const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); diff --git a/tests/dao/proposals/aibtc-payments-invoices-add-resource.test.ts b/tests/dao/proposals/aibtc-pmt-dao-toggle-resource.test.ts similarity index 91% rename from tests/dao/proposals/aibtc-payments-invoices-add-resource.test.ts rename to tests/dao/proposals/aibtc-pmt-dao-toggle-resource.test.ts index 93bcf4fc..9cfdabb1 100644 --- a/tests/dao/proposals/aibtc-payments-invoices-add-resource.test.ts +++ b/tests/dao/proposals/aibtc-pmt-dao-toggle-resource.test.ts @@ -4,7 +4,7 @@ import { OnchainMessagingErrCode } from "../../error-codes"; const accounts = simnet.getAccounts(); const deployer = accounts.get("deployer")!; -const contractName = "aibtc-payments-invoices-add-resource"; +const contractName = "aibtc-pmt-dao-toggle-resource"; const contractAddress = `${deployer}.${contractName}`; const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); diff --git a/tests/dao/proposals/aibtc-pmt-sbtc-add-resource.test.ts b/tests/dao/proposals/aibtc-pmt-sbtc-add-resource.test.ts new file mode 100644 index 00000000..85bec047 --- /dev/null +++ b/tests/dao/proposals/aibtc-pmt-sbtc-add-resource.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-pmt-sbtc-add-resource"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-pmt-sbtc-set-payment-address.test.ts b/tests/dao/proposals/aibtc-pmt-sbtc-set-payment-address.test.ts new file mode 100644 index 00000000..eedf3d75 --- /dev/null +++ b/tests/dao/proposals/aibtc-pmt-sbtc-set-payment-address.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-pmt-sbtc-set-payment-address"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-pmt-sbtc-toggle-resource-by-name.test.ts b/tests/dao/proposals/aibtc-pmt-sbtc-toggle-resource-by-name.test.ts new file mode 100644 index 00000000..3065d5cb --- /dev/null +++ b/tests/dao/proposals/aibtc-pmt-sbtc-toggle-resource-by-name.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-pmt-sbtc-toggle-resource-by-name"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-pmt-sbtc-toggle-resource.test.ts b/tests/dao/proposals/aibtc-pmt-sbtc-toggle-resource.test.ts new file mode 100644 index 00000000..ffff1342 --- /dev/null +++ b/tests/dao/proposals/aibtc-pmt-sbtc-toggle-resource.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-pmt-sbtc-toggle-resource"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-pmt-stx-add-resource.test.ts b/tests/dao/proposals/aibtc-pmt-stx-add-resource.test.ts new file mode 100644 index 00000000..e00fdd4c --- /dev/null +++ b/tests/dao/proposals/aibtc-pmt-stx-add-resource.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-pmt-stx-add-resource"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-pmt-stx-set-payment-address.test.ts b/tests/dao/proposals/aibtc-pmt-stx-set-payment-address.test.ts new file mode 100644 index 00000000..06f864ac --- /dev/null +++ b/tests/dao/proposals/aibtc-pmt-stx-set-payment-address.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-pmt-stx-set-payment-address"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-pmt-stx-toggle-resource-by-name.test.ts b/tests/dao/proposals/aibtc-pmt-stx-toggle-resource-by-name.test.ts new file mode 100644 index 00000000..086490f9 --- /dev/null +++ b/tests/dao/proposals/aibtc-pmt-stx-toggle-resource-by-name.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-pmt-stx-toggle-resource-by-name"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-pmt-stx-toggle-resource.test.ts b/tests/dao/proposals/aibtc-pmt-stx-toggle-resource.test.ts new file mode 100644 index 00000000..ece27835 --- /dev/null +++ b/tests/dao/proposals/aibtc-pmt-stx-toggle-resource.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-pmt-stx-toggle-resource"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-timed-vault-dao-initialize-new-vault.test.ts b/tests/dao/proposals/aibtc-timed-vault-dao-initialize-new-vault.test.ts new file mode 100644 index 00000000..01bb2424 --- /dev/null +++ b/tests/dao/proposals/aibtc-timed-vault-dao-initialize-new-vault.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-timed-vault-dao-initialize-new-vault"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-payments-invoices-toggle-resource-by-name.test.ts b/tests/dao/proposals/aibtc-timed-vault-dao-override-last-withdrawal-block.test.ts similarity index 89% rename from tests/dao/proposals/aibtc-payments-invoices-toggle-resource-by-name.test.ts rename to tests/dao/proposals/aibtc-timed-vault-dao-override-last-withdrawal-block.test.ts index 5046f4bc..9eece445 100644 --- a/tests/dao/proposals/aibtc-payments-invoices-toggle-resource-by-name.test.ts +++ b/tests/dao/proposals/aibtc-timed-vault-dao-override-last-withdrawal-block.test.ts @@ -4,7 +4,7 @@ import { OnchainMessagingErrCode } from "../../error-codes"; const accounts = simnet.getAccounts(); const deployer = accounts.get("deployer")!; -const contractName = "aibtc-payments-invoices-toggle-resource-by-name"; +const contractName = "aibtc-timed-vault-dao-override-last-withdrawal-block"; const contractAddress = `${deployer}.${contractName}`; const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); diff --git a/tests/dao/proposals/aibtc-timed-vault-set-withdrawal-amount.test.ts b/tests/dao/proposals/aibtc-timed-vault-dao-set-account-holder.test.ts similarity index 91% rename from tests/dao/proposals/aibtc-timed-vault-set-withdrawal-amount.test.ts rename to tests/dao/proposals/aibtc-timed-vault-dao-set-account-holder.test.ts index 5be9e1a9..e1f71721 100644 --- a/tests/dao/proposals/aibtc-timed-vault-set-withdrawal-amount.test.ts +++ b/tests/dao/proposals/aibtc-timed-vault-dao-set-account-holder.test.ts @@ -4,7 +4,7 @@ import { OnchainMessagingErrCode } from "../../error-codes"; const accounts = simnet.getAccounts(); const deployer = accounts.get("deployer")!; -const contractName = "aibtc-timed-vault-set-withdrawal-amount"; +const contractName = "aibtc-timed-vault-dao-set-account-holder"; const contractAddress = `${deployer}.${contractName}`; const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); diff --git a/tests/dao/proposals/aibtc-payments-invoices-set-payment-address.test.ts b/tests/dao/proposals/aibtc-timed-vault-dao-set-withdrawal-amount.test.ts similarity index 90% rename from tests/dao/proposals/aibtc-payments-invoices-set-payment-address.test.ts rename to tests/dao/proposals/aibtc-timed-vault-dao-set-withdrawal-amount.test.ts index 5c21099f..02a87584 100644 --- a/tests/dao/proposals/aibtc-payments-invoices-set-payment-address.test.ts +++ b/tests/dao/proposals/aibtc-timed-vault-dao-set-withdrawal-amount.test.ts @@ -4,7 +4,7 @@ import { OnchainMessagingErrCode } from "../../error-codes"; const accounts = simnet.getAccounts(); const deployer = accounts.get("deployer")!; -const contractName = "aibtc-payments-invoices-set-payment-address"; +const contractName = "aibtc-timed-vault-dao-set-withdrawal-amount"; const contractAddress = `${deployer}.${contractName}`; const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); diff --git a/tests/dao/proposals/aibtc-timed-vault-dao-set-withdrawal-period.test.ts b/tests/dao/proposals/aibtc-timed-vault-dao-set-withdrawal-period.test.ts new file mode 100644 index 00000000..d3ebdfb4 --- /dev/null +++ b/tests/dao/proposals/aibtc-timed-vault-dao-set-withdrawal-period.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-timed-vault-dao-set-withdrawal-period"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-timed-vault-dao-withdraw.test.ts b/tests/dao/proposals/aibtc-timed-vault-dao-withdraw.test.ts new file mode 100644 index 00000000..7b982b06 --- /dev/null +++ b/tests/dao/proposals/aibtc-timed-vault-dao-withdraw.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; +import { ContractProposalType } from "../../dao-types"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractAddress = `${deployer}.${ContractProposalType.DAO_TIMED_VAULT_DAO_WITHDRAW}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${ContractProposalType.DAO_TIMED_VAULT_DAO_WITHDRAW}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-timed-vault-override-last-withdrawal-block.test.ts b/tests/dao/proposals/aibtc-timed-vault-override-last-withdrawal-block.test.ts deleted file mode 100644 index 39ea2eba..00000000 --- a/tests/dao/proposals/aibtc-timed-vault-override-last-withdrawal-block.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Cl } from "@stacks/transactions"; -import { describe, expect, it } from "vitest"; -import { OnchainMessagingErrCode } from "../../error-codes"; - -const accounts = simnet.getAccounts(); -const deployer = accounts.get("deployer")!; -const contractName = "aibtc-timed-vault-override-last-withdrawal-block"; -const contractAddress = `${deployer}.${contractName}`; - -const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); - -describe(`core proposal: ${contractName}`, () => { - it("execute() fails if called directly", () => { - const receipt = simnet.callPublicFn( - contractAddress, - "execute", - [Cl.principal(deployer)], - deployer - ); - expect(receipt.result).toBeErr(expectedErr); - }); -}); diff --git a/tests/dao/proposals/aibtc-timed-vault-sbtc-initialize-new-vault.test.ts b/tests/dao/proposals/aibtc-timed-vault-sbtc-initialize-new-vault.test.ts new file mode 100644 index 00000000..aee4e94e --- /dev/null +++ b/tests/dao/proposals/aibtc-timed-vault-sbtc-initialize-new-vault.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-timed-vault-sbtc-initialize-new-vault"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-timed-vault-sbtc-override-last-withdrawal-block.test.ts b/tests/dao/proposals/aibtc-timed-vault-sbtc-override-last-withdrawal-block.test.ts new file mode 100644 index 00000000..96ce215a --- /dev/null +++ b/tests/dao/proposals/aibtc-timed-vault-sbtc-override-last-withdrawal-block.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-timed-vault-sbtc-override-last-withdrawal-block"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-timed-vault-sbtc-set-account-holder.test.ts b/tests/dao/proposals/aibtc-timed-vault-sbtc-set-account-holder.test.ts new file mode 100644 index 00000000..b639e68a --- /dev/null +++ b/tests/dao/proposals/aibtc-timed-vault-sbtc-set-account-holder.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-timed-vault-sbtc-set-account-holder"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-timed-vault-sbtc-set-withdrawal-amount.test.ts b/tests/dao/proposals/aibtc-timed-vault-sbtc-set-withdrawal-amount.test.ts new file mode 100644 index 00000000..e816ec7a --- /dev/null +++ b/tests/dao/proposals/aibtc-timed-vault-sbtc-set-withdrawal-amount.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-timed-vault-sbtc-set-withdrawal-amount"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-timed-vault-sbtc-set-withdrawal-period.test.ts b/tests/dao/proposals/aibtc-timed-vault-sbtc-set-withdrawal-period.test.ts new file mode 100644 index 00000000..3b2fb10a --- /dev/null +++ b/tests/dao/proposals/aibtc-timed-vault-sbtc-set-withdrawal-period.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-timed-vault-sbtc-set-withdrawal-period"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-timed-vault-sbtc-withdraw.test.ts b/tests/dao/proposals/aibtc-timed-vault-sbtc-withdraw.test.ts new file mode 100644 index 00000000..521136ae --- /dev/null +++ b/tests/dao/proposals/aibtc-timed-vault-sbtc-withdraw.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; +import { ContractProposalType } from "../../dao-types"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractAddress = `${deployer}.${ContractProposalType.DAO_TIMED_VAULT_SBTC_WITHDRAW}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${ContractProposalType.DAO_TIMED_VAULT_SBTC_WITHDRAW}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-timed-vault-set-withdrawal-period.test.ts b/tests/dao/proposals/aibtc-timed-vault-set-withdrawal-period.test.ts deleted file mode 100644 index d06067c5..00000000 --- a/tests/dao/proposals/aibtc-timed-vault-set-withdrawal-period.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Cl } from "@stacks/transactions"; -import { describe, expect, it } from "vitest"; -import { OnchainMessagingErrCode } from "../../error-codes"; - -const accounts = simnet.getAccounts(); -const deployer = accounts.get("deployer")!; -const contractName = "aibtc-timed-vault-set-withdrawal-period"; -const contractAddress = `${deployer}.${contractName}`; - -const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); - -describe(`core proposal: ${contractName}`, () => { - it("execute() fails if called directly", () => { - const receipt = simnet.callPublicFn( - contractAddress, - "execute", - [Cl.principal(deployer)], - deployer - ); - expect(receipt.result).toBeErr(expectedErr); - }); -}); diff --git a/tests/dao/proposals/aibtc-timed-vault-initialize-new-account.test.ts b/tests/dao/proposals/aibtc-timed-vault-stx-initialize-new-vault.test.ts similarity index 91% rename from tests/dao/proposals/aibtc-timed-vault-initialize-new-account.test.ts rename to tests/dao/proposals/aibtc-timed-vault-stx-initialize-new-vault.test.ts index 7f52a112..1d67d3ee 100644 --- a/tests/dao/proposals/aibtc-timed-vault-initialize-new-account.test.ts +++ b/tests/dao/proposals/aibtc-timed-vault-stx-initialize-new-vault.test.ts @@ -4,7 +4,7 @@ import { OnchainMessagingErrCode } from "../../error-codes"; const accounts = simnet.getAccounts(); const deployer = accounts.get("deployer")!; -const contractName = "aibtc-timed-vault-initialize-new-account"; +const contractName = "aibtc-timed-vault-stx-initialize-new-vault"; const contractAddress = `${deployer}.${contractName}`; const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); diff --git a/tests/dao/proposals/aibtc-timed-vault-stx-override-last-withdrawal-block.test.ts b/tests/dao/proposals/aibtc-timed-vault-stx-override-last-withdrawal-block.test.ts new file mode 100644 index 00000000..700ba116 --- /dev/null +++ b/tests/dao/proposals/aibtc-timed-vault-stx-override-last-withdrawal-block.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-timed-vault-stx-override-last-withdrawal-block"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-timed-vault-stx-set-account-holder.test.ts b/tests/dao/proposals/aibtc-timed-vault-stx-set-account-holder.test.ts new file mode 100644 index 00000000..8b22ee24 --- /dev/null +++ b/tests/dao/proposals/aibtc-timed-vault-stx-set-account-holder.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-timed-vault-stx-set-account-holder"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-timed-vault-stx-set-withdrawal-amount.test.ts b/tests/dao/proposals/aibtc-timed-vault-stx-set-withdrawal-amount.test.ts new file mode 100644 index 00000000..eae35f76 --- /dev/null +++ b/tests/dao/proposals/aibtc-timed-vault-stx-set-withdrawal-amount.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-timed-vault-stx-set-withdrawal-amount"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-timed-vault-stx-set-withdrawal-period.test.ts b/tests/dao/proposals/aibtc-timed-vault-stx-set-withdrawal-period.test.ts new file mode 100644 index 00000000..ebb5432b --- /dev/null +++ b/tests/dao/proposals/aibtc-timed-vault-stx-set-withdrawal-period.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-timed-vault-stx-set-withdrawal-period"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-timed-vault-stx-withdraw.test.ts b/tests/dao/proposals/aibtc-timed-vault-stx-withdraw.test.ts new file mode 100644 index 00000000..410ec2b7 --- /dev/null +++ b/tests/dao/proposals/aibtc-timed-vault-stx-withdraw.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; +import { ContractProposalType } from "../../dao-types"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractAddress = `${deployer}.${ContractProposalType.DAO_TIMED_VAULT_STX_WITHDRAW}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${ContractProposalType.DAO_TIMED_VAULT_STX_WITHDRAW}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/aibtc-treasury-allow-assets.test.ts b/tests/dao/proposals/aibtc-treasury-allow-assets.test.ts new file mode 100644 index 00000000..8eb2af90 --- /dev/null +++ b/tests/dao/proposals/aibtc-treasury-allow-assets.test.ts @@ -0,0 +1,22 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it } from "vitest"; +import { OnchainMessagingErrCode } from "../../error-codes"; + +const accounts = simnet.getAccounts(); +const deployer = accounts.get("deployer")!; +const contractName = "aibtc-treasury-allow-assets"; +const contractAddress = `${deployer}.${contractName}`; + +const expectedErr = Cl.uint(OnchainMessagingErrCode.ERR_UNAUTHORIZED); + +describe(`core proposal: ${contractName}`, () => { + it("execute() fails if called directly", () => { + const receipt = simnet.callPublicFn( + contractAddress, + "execute", + [Cl.principal(deployer)], + deployer + ); + expect(receipt.result).toBeErr(expectedErr); + }); +}); diff --git a/tests/dao/proposals/test-all-core-proposals.test.ts b/tests/dao/proposals/test-all-core-proposals.test.ts index 7dc5112e..4c4cf848 100644 --- a/tests/dao/proposals/test-all-core-proposals.test.ts +++ b/tests/dao/proposals/test-all-core-proposals.test.ts @@ -5,6 +5,7 @@ import { dbgLog, getDaoTokens, passCoreProposal, + SBTC_CONTRACT, VOTING_CONFIG, } from "../../test-utilities"; import { @@ -22,6 +23,9 @@ const getContract = ( ): string => `${deployer}.${contractType}`; const baseDaoContractAddress = getContract(ContractType.DAO_BASE); +const oldBootstrapContractAddress = getContract( + ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION +); const bootstrapContractAddress = getContract( ContractProposalType.DAO_BASE_BOOTSTRAP_INITIALIZATION_V2 ); @@ -31,45 +35,29 @@ const coreProposalsV2ContractAddress = getContract( const tokenContractAddress = getContract(ContractType.DAO_TOKEN); const tokenDexContractAddress = getContract(ContractType.DAO_TOKEN_DEX); const treasuryContractAddress = getContract(ContractType.DAO_TREASURY); +const timedVaultStxContractAddress = getContract( + ContractType.DAO_TIMED_VAULT_STX +); +const timedVaultSbtcContractAddress = getContract( + ContractType.DAO_TIMED_VAULT_SBTC +); +const timedVaultDaoContractAddress = getContract( + ContractType.DAO_TIMED_VAULT_DAO +); const voteSettings = VOTING_CONFIG[ContractType.DAO_CORE_PROPOSALS_V2]; -const proposals = [ - getContract(ContractProposalType.DAO_ACTION_PROPOSALS_SET_PROPOSAL_BOND), - getContract(ContractProposalType.DAO_BASE_ADD_NEW_EXTENSION), - getContract(ContractProposalType.DAO_BASE_DISABLE_EXTENSION), - getContract(ContractProposalType.DAO_BASE_ENABLE_EXTENSION), - getContract(ContractProposalType.DAO_BASE_REPLACE_EXTENSION), - getContract(ContractProposalType.DAO_BASE_REPLACE_EXTENSION_PROPOSAL_VOTING), - getContract(ContractProposalType.DAO_CORE_PROPOSALS_SET_PROPOSAL_BOND), - getContract(ContractProposalType.DAO_ONCHAIN_MESSAGING_SEND), - getContract(ContractProposalType.DAO_PAYMENTS_INVOICES_ADD_RESOURCE), - getContract(ContractProposalType.DAO_PAYMENTS_INVOICES_SET_PAYMENT_ADDRESS), - getContract(ContractProposalType.DAO_PAYMENTS_INVOICES_TOGGLE_RESOURCE), - getContract( - ContractProposalType.DAO_PAYMENTS_INVOICES_TOGGLE_RESOURCE_BY_NAME - ), - getContract(ContractProposalType.DAO_TIMED_VAULT_INITIALIZE_NEW_ACCOUNT), - getContract( - ContractProposalType.DAO_TIMED_VAULT_OVERRIDE_LAST_WITHDRAWAL_BLOCK - ), - getContract(ContractProposalType.DAO_TIMED_VAULT_SET_ACCOUNT_HOLDER), - getContract(ContractProposalType.DAO_TIMED_VAULT_SET_WITHDRAWAL_AMOUNT), - getContract(ContractProposalType.DAO_TIMED_VAULT_SET_WITHDRAWAL_PERIOD), - getContract(ContractProposalType.DAO_TOKEN_OWNER_SET_TOKEN_URI), - getContract(ContractProposalType.DAO_TOKEN_OWNER_TRANSFER_OWNERSHIP), - getContract(ContractProposalType.DAO_TREASURY_ALLOW_ASSET), - getContract(ContractProposalType.DAO_TREASURY_DELEGATE_STX), - getContract(ContractProposalType.DAO_TREASURY_DISABLE_ASSET), - getContract(ContractProposalType.DAO_TREASURY_REVOKE_DELEGATION), - getContract(ContractProposalType.DAO_TREASURY_WITHDRAW_FT), - getContract(ContractProposalType.DAO_TREASURY_WITHDRAW_NFT), - getContract(ContractProposalType.DAO_TREASURY_WITHDRAW_STX), -]; +// create an array of all the proposals +const proposals = Object.values(ContractProposalType).map((proposal) => + getContract(proposal) +); describe("Core proposal testing: all contracts", () => { it("should pass all core proposals", () => { // arrange + const amountDao = 10000000000000; // 100,000 dao tokens (8 decimals) + const sbtcSpend = 400000; // used to buy dao tokens from dex + const amountStx = 1000000000; // 1,000 STX (6 decimals) // construct the dao const constructReceipt = constructDao( deployer, @@ -77,22 +65,80 @@ describe("Core proposal testing: all contracts", () => { bootstrapContractAddress ); expect(constructReceipt.result).toBeOk(Cl.bool(true)); - // get the dao tokens so we can propose + // get sbtc to transfer to the treasury and vault + const sbtcReceipt = simnet.callPublicFn( + SBTC_CONTRACT, + "faucet", + [], + deployer + ); + expect(sbtcReceipt.result).toBeOk(Cl.bool(true)); + // get dao tokens to transfer to the treasury, vault, and to vote const dexReceipt = getDaoTokens( tokenContractAddress, tokenDexContractAddress, deployer, - 1000000 + sbtcSpend ); expect(dexReceipt.result).toBeOk(Cl.bool(true)); - // deposit STX to the treasury for proposals - const treasuryReceipt = simnet.callPublicFn( + // transfer dao tokens to the treasury + const daoTransferReceipt = simnet.callPublicFn( + tokenContractAddress, + "transfer", + [ + Cl.uint(amountDao), + Cl.principal(deployer), + Cl.principal(treasuryContractAddress), + Cl.none(), + ], + deployer + ); + expect(daoTransferReceipt.result).toBeOk(Cl.bool(true)); + // transfer sbtc to the treasury + const sbtcTransferReceipt = simnet.callPublicFn( + SBTC_CONTRACT, + "transfer", + [ + Cl.uint(sbtcSpend), + Cl.principal(deployer), + Cl.principal(treasuryContractAddress), + Cl.none(), + ], + deployer + ); + expect(sbtcTransferReceipt.result).toBeOk(Cl.bool(true)); + // transfer stx to the treasury + const stxTransferReceipt = simnet.callPublicFn( treasuryContractAddress, "deposit-stx", - [Cl.uint(1000000000)], + [Cl.uint(amountStx)], + deployer + ); + expect(stxTransferReceipt.result).toBeOk(Cl.bool(true)); + // transfer stx to the stx vault + const stxVaultTransferReceipt = simnet.callPublicFn( + timedVaultStxContractAddress, + "deposit", + [Cl.uint(amountStx)], + deployer + ); + expect(stxVaultTransferReceipt.result).toBeOk(Cl.bool(true)); + // transfer sbtc to the sbtc vault + const sbtcVaultTransferReceipt = simnet.callPublicFn( + timedVaultSbtcContractAddress, + "deposit", + [Cl.uint(sbtcSpend)], + deployer + ); + expect(sbtcVaultTransferReceipt.result).toBeOk(Cl.bool(true)); + // transfer dao tokens to the dao vault + const daoVaultTransferReceipt = simnet.callPublicFn( + timedVaultDaoContractAddress, + "deposit", + [Cl.uint(amountDao)], deployer ); - expect(treasuryReceipt.result).toBeOk(Cl.bool(true)); + expect(daoVaultTransferReceipt.result).toBeOk(Cl.bool(true)); // mint nft to the treasury const nftId = 1; const mintNftReceipt = simnet.callPublicFn( @@ -101,11 +147,23 @@ describe("Core proposal testing: all contracts", () => { [Cl.principal(treasuryContractAddress)], deployer ); - dbgLog(`result: ${JSON.stringify(cvToValue(mintNftReceipt.result))}`); - dbgLog(`mintNftReceipt: ${JSON.stringify(mintNftReceipt, null, 2)}`); expect(mintNftReceipt.result).toBeOk(Cl.bool(true)); + // output assets map for debugging + const assetsMap = simnet.getAssetsMap(); + for (const [key, value] of assetsMap) { + for (const [innerKey, innerValue] of value) { + dbgLog(`assetsMap[${key}]: ${innerKey}: ${innerValue}`); + } + } // act and assert proposals.forEach((proposal) => { + // skip the bootstrap proposals + if ( + proposal === oldBootstrapContractAddress || + proposal === bootstrapContractAddress + ) { + return; + } dbgLog("====================================="); dbgLog(`Testing proposal: ${proposal}`); dbgLog( diff --git a/tests/error-codes.ts b/tests/error-codes.ts index 9a292ac9..286b1b0c 100644 --- a/tests/error-codes.ts +++ b/tests/error-codes.ts @@ -46,10 +46,12 @@ export enum ActionErrCode { } export enum TimedVaultErrCode { - ERR_INVALID = 2000, - ERR_UNAUTHORIZED, + ERR_NOT_DAO_OR_EXTENSION = 2000, + ERR_INVALID, + ERR_NOT_ACCOUNT_HOLDER, ERR_TOO_SOON, ERR_INVALID_AMOUNT, + ERR_FETCHING_BALANCE, } export enum CoreProposalErrCode { @@ -92,8 +94,8 @@ export enum OnchainMessagingErrCode { ERR_UNAUTHORIZED, } -export enum PaymentsInvoicesErrCode { - ERR_UNAUTHORIZED = 5000, +export enum PaymentProcessorErrCode { + ERR_NOT_DAO_OR_EXTENSION = 5000, ERR_INVALID_PARAMS, ERR_NAME_ALREADY_USED, ERR_SAVING_RESOURCE_DATA, diff --git a/tests/test-utilities.ts b/tests/test-utilities.ts index eecae8fd..38eb0c28 100644 --- a/tests/test-utilities.ts +++ b/tests/test-utilities.ts @@ -92,22 +92,31 @@ export function generateContractNames(tokenSymbol: string): ContractNames { [ContractType.DAO_TOKEN_DEX]: `${tokenSymbol.toLowerCase()}-faktory-dex`, [ContractType.DAO_TOKEN_OWNER]: `${tokenSymbol.toLowerCase()}-token-owner`, [ContractType.DAO_BASE]: `${tokenSymbol.toLowerCase()}-base-dao`, + [ContractType.DAO_TOKEN_PRE_DEX]: `${tokenSymbol.toLowerCase()}-faktory-pre-dex`, [ContractType.DAO_ACTION_PROPOSALS]: `${tokenSymbol.toLowerCase()}-action-proposals`, [ContractType.DAO_ACTION_PROPOSALS_V2]: `${tokenSymbol.toLowerCase()}-action-proposals-v2`, - [ContractType.DAO_TIMED_VAULT]: `${tokenSymbol.toLowerCase()}-timed-vault`, + [ContractType.DAO_TIMED_VAULT_DAO]: `${tokenSymbol.toLowerCase()}-timed-vault-dao`, + [ContractType.DAO_TIMED_VAULT_SBTC]: `${tokenSymbol.toLowerCase()}-timed-vault-sbtc`, + [ContractType.DAO_TIMED_VAULT_STX]: `${tokenSymbol.toLowerCase()}-timed-vault-stx`, [ContractType.DAO_CHARTER]: `${tokenSymbol.toLowerCase()}-dao-charter`, [ContractType.DAO_CORE_PROPOSALS]: `${tokenSymbol.toLowerCase()}-core-proposals`, [ContractType.DAO_CORE_PROPOSALS_V2]: `${tokenSymbol.toLowerCase()}-core-proposals-v2`, [ContractType.DAO_MESSAGING]: `${tokenSymbol.toLowerCase()}-onchain-messaging`, - [ContractType.DAO_PAYMENTS]: `${tokenSymbol.toLowerCase()}-payments-invoices`, + [ContractType.DAO_PAYMENT_PROCESSOR_DAO]: `${tokenSymbol.toLowerCase()}-payment-processor-dao`, + [ContractType.DAO_PAYMENT_PROCESSOR_SBTC]: `${tokenSymbol.toLowerCase()}-payment-processor-sbtc`, + [ContractType.DAO_PAYMENT_PROCESSOR_STX]: `${tokenSymbol.toLowerCase()}-payment-processor-stx`, [ContractType.DAO_TREASURY]: `${tokenSymbol.toLowerCase()}-treasury`, - [ContractActionType.DAO_ACTION_ADD_RESOURCE]: `${tokenSymbol.toLowerCase()}-action-add-resource`, [ContractActionType.DAO_ACTION_ALLOW_ASSET]: `${tokenSymbol.toLowerCase()}-action-allow-asset`, [ContractActionType.DAO_ACTION_SEND_MESSAGE]: `${tokenSymbol.toLowerCase()}-action-send-message`, - [ContractActionType.DAO_ACTION_SET_ACCOUNT_HOLDER]: `${tokenSymbol.toLowerCase()}-action-set-account-holder`, - [ContractActionType.DAO_ACTION_SET_WITHDRAWAL_AMOUNT]: `${tokenSymbol.toLowerCase()}-action-set-withdrawal-amount`, - [ContractActionType.DAO_ACTION_SET_WITHDRAWAL_PERIOD]: `${tokenSymbol.toLowerCase()}-action-set-withdrawal-period`, - [ContractActionType.DAO_ACTION_TOGGLE_RESOURCE_BY_NAME]: `${tokenSymbol.toLowerCase()}-action-toggle-resource`, + [ContractActionType.DAO_ACTION_CONFIGURE_TIMED_VAULT_DAO]: `${tokenSymbol.toLowerCase()}-action-configure-timed-vault-dao`, + [ContractActionType.DAO_ACTION_CONFIGURE_TIMED_VAULT_SBTC]: `${tokenSymbol.toLowerCase()}-action-configure-timed-vault-sbtc`, + [ContractActionType.DAO_ACTION_CONFIGURE_TIMED_VAULT_STX]: `${tokenSymbol.toLowerCase()}-action-configure-timed-vault-stx`, + [ContractActionType.DAO_ACTION_PMT_DAO_ADD_RESOURCE]: `${tokenSymbol.toLowerCase()}-action-payments-dao-add-resource`, + [ContractActionType.DAO_ACTION_PMT_DAO_TOGGLE_RESOURCE]: `${tokenSymbol.toLowerCase()}-action-payments-dao-toggle-resource`, + [ContractActionType.DAO_ACTION_PMT_SBTC_ADD_RESOURCE]: `${tokenSymbol.toLowerCase()}-action-payments-sbtc-add-resource`, + [ContractActionType.DAO_ACTION_PMT_SBTC_TOGGLE_RESOURCE]: `${tokenSymbol.toLowerCase()}-action-payments-sbtc-toggle-resource`, + [ContractActionType.DAO_ACTION_PMT_STX_ADD_RESOURCE]: `${tokenSymbol.toLowerCase()}-action-payments-stx-add-resource`, + [ContractActionType.DAO_ACTION_PMT_STX_TOGGLE_RESOURCE]: `${tokenSymbol.toLowerCase()}-action-payments-stx-toggle-resource`, }; } @@ -220,6 +229,8 @@ export function constructDao( return constructDaoReceipt; } +const memoContext = "Can pass up to 1024 characters for additional context."; + export function passCoreProposal( coreProposalsContractAddress: string, proposalContractAddress: string, @@ -234,7 +245,10 @@ export function passCoreProposal( const createProposalReceipt = simnet.callPublicFn( coreProposalsContractAddress, "create-proposal", - [Cl.principal(proposalContractAddress)], + [ + Cl.principal(proposalContractAddress), + Cl.some(Cl.stringAscii(memoContext)), + ], sender ); expect(createProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -279,7 +293,10 @@ export function failCoreProposal( const createProposalReceipt = simnet.callPublicFn( coreProposalsContractAddress, "create-proposal", - [Cl.principal(proposalContractAddress)], + [ + Cl.principal(proposalContractAddress), + Cl.some(Cl.stringAscii(memoContext)), + ], sender ); expect(createProposalReceipt.result).toBeOk(Cl.bool(true)); @@ -327,6 +344,7 @@ export function passActionProposal( [ Cl.principal(proposalContractAddress), Cl.buffer(Cl.serialize(proposalParams)), + Cl.some(Cl.stringAscii(memoContext)), ], sender );