diff --git a/Clarinet.toml b/Clarinet.toml index 409d7f5..8a4e95b 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -73,7 +73,8 @@ epoch = "3.0" # Agent contracts (depend on base-dao) [contracts.agent-registry] path = "contracts/agent/agent-registry.clar" -epoch = "3.0" +epoch = "3.3" +clarity_version = 4 [contracts.agent-account] path = "contracts/agent/agent-account.clar" diff --git a/contracts/agent/agent-account.clar b/contracts/agent/agent-account.clar index 27b8243..bd1da54 100644 --- a/contracts/agent/agent-account.clar +++ b/contracts/agent/agent-account.clar @@ -1,7 +1,8 @@ ;; title: agent-account -;; version: 1.0.0 +;; version: 2.0.0 ;; summary: A user-agent account contract for managing assets and DAO interactions. -;; description: Hardcoded owner and agent principals with bit-based permissions. +;; description: Deploy identical code, initialize once with owner and agent principals. +;; Same source code produces same contract-hash for registry verification. ;; The owner has full access; the agent can perform allowed actions. ;; Funds are always withdrawn to the owner address. @@ -20,10 +21,7 @@ (define-constant DEPLOYED_BURN_BLOCK burn-block-height) (define-constant DEPLOYED_STACKS_BLOCK stacks-block-height) (define-constant SELF (as-contract tx-sender)) - -;; Hardcoded owner and agent addresses (template - deployer customizes) -(define-constant ACCOUNT_OWNER 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM) -(define-constant ACCOUNT_AGENT 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG) +(define-constant DEPLOYER tx-sender) ;; Error codes (define-constant ERR_CALLER_NOT_OWNER (err u4000)) @@ -31,6 +29,8 @@ (define-constant ERR_CONTRACT_NOT_APPROVED (err u4002)) (define-constant ERR_INVALID_APPROVAL_TYPE (err u4003)) (define-constant ERR_ZERO_AMOUNT (err u4004)) +(define-constant ERR_ALREADY_INITIALIZED (err u4005)) +(define-constant ERR_NOT_INITIALIZED (err u4006)) ;; Permission flags (bit-based) (define-constant PERMISSION_MANAGE_ASSETS (pow u2 u0)) ;; 1 @@ -57,11 +57,40 @@ ;; DATA VARS +;; Account configuration (set once via initialize) +(define-data-var initialized bool false) +(define-data-var account-owner (optional principal) none) +(define-data-var account-agent (optional principal) none) + ;; Current agent permissions (can be modified by owner) (define-data-var agentPermissions uint DEFAULT_PERMISSIONS) ;; PUBLIC FUNCTIONS +;; ============================================================ +;; INITIALIZATION (deployer only, one-time) +;; ============================================================ + +(define-public (initialize (owner principal) (agent principal)) + (begin + (asserts! (is-eq tx-sender DEPLOYER) ERR_CALLER_NOT_OWNER) + (asserts! (not (var-get initialized)) ERR_ALREADY_INITIALIZED) + (var-set account-owner (some owner)) + (var-set account-agent (some agent)) + (var-set initialized true) + (print { + notification: "agent-account/initialized", + payload: { + account: SELF, + owner: owner, + agent: agent, + deployer: DEPLOYER + } + }) + (ok true) + ) +) + ;; ============================================================ ;; ASSET MANAGEMENT ;; ============================================================ @@ -69,6 +98,7 @@ ;; Deposit STX to the agent account (define-public (deposit-stx (amount uint)) (begin + (asserts! (var-get initialized) ERR_NOT_INITIALIZED) (asserts! (> amount u0) ERR_ZERO_AMOUNT) (asserts! (manage-assets-allowed) ERR_OPERATION_NOT_ALLOWED) (print { @@ -87,6 +117,7 @@ ;; Deposit fungible tokens to the agent account (define-public (deposit-ft (ft ) (amount uint)) (begin + (asserts! (var-get initialized) ERR_NOT_INITIALIZED) (asserts! (> amount u0) ERR_ZERO_AMOUNT) (asserts! (manage-assets-allowed) ERR_OPERATION_NOT_ALLOWED) (print { @@ -105,7 +136,7 @@ ;; Withdraw STX from the agent account (always to owner) (define-public (withdraw-stx (amount uint)) - (begin + (let ((owner (unwrap! (var-get account-owner) ERR_NOT_INITIALIZED))) (asserts! (> amount u0) ERR_ZERO_AMOUNT) (asserts! (manage-assets-allowed) ERR_OPERATION_NOT_ALLOWED) (print { @@ -114,17 +145,17 @@ amount: amount, sender: SELF, caller: contract-caller, - recipient: ACCOUNT_OWNER + recipient: owner } }) - (as-contract (stx-transfer? amount SELF ACCOUNT_OWNER)) + (as-contract (stx-transfer? amount SELF owner)) ) ) ;; Withdraw fungible tokens from the agent account (always to owner) ;; Token must be approved (define-public (withdraw-ft (ft ) (amount uint)) - (begin + (let ((owner (unwrap! (var-get account-owner) ERR_NOT_INITIALIZED))) (asserts! (> amount u0) ERR_ZERO_AMOUNT) (asserts! (manage-assets-allowed) ERR_OPERATION_NOT_ALLOWED) (asserts! (is-approved-contract (contract-of ft) APPROVED_CONTRACT_TOKEN) @@ -137,10 +168,10 @@ assetContract: (contract-of ft), sender: SELF, caller: contract-caller, - recipient: ACCOUNT_OWNER + recipient: owner } }) - (as-contract (contract-call? ft transfer amount SELF ACCOUNT_OWNER none)) + (as-contract (contract-call? ft transfer amount SELF owner none)) ) ) @@ -155,6 +186,7 @@ (memo (optional (string-ascii 1024))) ) (begin + (asserts! (var-get initialized) ERR_NOT_INITIALIZED) (asserts! (use-proposals-allowed) ERR_OPERATION_NOT_ALLOWED) (asserts! (is-approved-contract (contract-of votingContract) APPROVED_CONTRACT_VOTING) @@ -180,6 +212,7 @@ (vote bool) ) (begin + (asserts! (var-get initialized) ERR_NOT_INITIALIZED) (asserts! (use-proposals-allowed) ERR_OPERATION_NOT_ALLOWED) (asserts! (is-approved-contract (contract-of votingContract) APPROVED_CONTRACT_VOTING) @@ -206,6 +239,7 @@ (proposal ) ) (begin + (asserts! (var-get initialized) ERR_NOT_INITIALIZED) (asserts! (use-proposals-allowed) ERR_OPERATION_NOT_ALLOWED) (asserts! (is-approved-contract (contract-of votingContract) APPROVED_CONTRACT_VOTING) @@ -232,6 +266,7 @@ ;; Approve a contract for use with the agent account (define-public (approve-contract (contract principal) (type uint)) (begin + (asserts! (var-get initialized) ERR_NOT_INITIALIZED) (asserts! (is-valid-type type) ERR_INVALID_APPROVAL_TYPE) (asserts! (approve-revoke-contract-allowed) ERR_OPERATION_NOT_ALLOWED) (print { @@ -251,6 +286,7 @@ ;; Revoke a contract from use with the agent account (define-public (revoke-contract (contract principal) (type uint)) (begin + (asserts! (var-get initialized) ERR_NOT_INITIALIZED) (asserts! (is-valid-type type) ERR_INVALID_APPROVAL_TYPE) (asserts! (approve-revoke-contract-allowed) ERR_OPERATION_NOT_ALLOWED) (print { @@ -274,6 +310,7 @@ ;; Toggle agent's ability to manage assets (deposit/withdraw) (define-public (set-agent-can-manage-assets (canManage bool)) (begin + (asserts! (var-get initialized) ERR_NOT_INITIALIZED) (asserts! (is-owner) ERR_CALLER_NOT_OWNER) (print { notification: "agent-account/set-agent-can-manage-assets", @@ -297,6 +334,7 @@ ;; Toggle agent's ability to use proposal functions (define-public (set-agent-can-use-proposals (canUseProposals bool)) (begin + (asserts! (var-get initialized) ERR_NOT_INITIALIZED) (asserts! (is-owner) ERR_CALLER_NOT_OWNER) (print { notification: "agent-account/set-agent-can-use-proposals", @@ -320,6 +358,7 @@ ;; Toggle agent's ability to approve/revoke contracts (define-public (set-agent-can-approve-revoke-contracts (canApproveRevoke bool)) (begin + (asserts! (var-get initialized) ERR_NOT_INITIALIZED) (asserts! (is-owner) ERR_CALLER_NOT_OWNER) (print { notification: "agent-account/set-agent-can-approve-revoke-contracts", @@ -343,6 +382,7 @@ ;; Toggle agent's ability to buy/sell assets (define-public (set-agent-can-buy-sell-assets (canBuySell bool)) (begin + (asserts! (var-get initialized) ERR_NOT_INITIALIZED) (asserts! (is-owner) ERR_CALLER_NOT_OWNER) (print { notification: "agent-account/set-agent-can-buy-sell-assets", @@ -365,13 +405,18 @@ ;; Get config (implements trait) (define-public (get-config) - (ok { - account: SELF, - agent: ACCOUNT_AGENT, - owner: ACCOUNT_OWNER, - agent-can-manage-assets: (not (is-eq u0 (bit-and (var-get agentPermissions) PERMISSION_MANAGE_ASSETS))), - agent-can-use-proposals: (not (is-eq u0 (bit-and (var-get agentPermissions) PERMISSION_USE_PROPOSALS))) - }) + (let ( + (owner (unwrap! (var-get account-owner) ERR_NOT_INITIALIZED)) + (agent (unwrap! (var-get account-agent) ERR_NOT_INITIALIZED)) + ) + (ok { + account: SELF, + agent: agent, + owner: owner, + agent-can-manage-assets: (not (is-eq u0 (bit-and (var-get agentPermissions) PERMISSION_MANAGE_ASSETS))), + agent-can-use-proposals: (not (is-eq u0 (bit-and (var-get agentPermissions) PERMISSION_USE_PROPOSALS))) + }) + ) ) ;; ============================================================ @@ -389,8 +434,10 @@ (define-read-only (get-configuration) { account: SELF, - owner: ACCOUNT_OWNER, - agent: ACCOUNT_AGENT, + owner: (var-get account-owner), + agent: (var-get account-agent), + initialized: (var-get initialized), + deployer: DEPLOYER, deployedBurnBlock: DEPLOYED_BURN_BLOCK, deployedStacksBlock: DEPLOYED_STACKS_BLOCK } @@ -435,12 +482,18 @@ ;; Check if caller is the account owner (define-private (is-owner) - (is-eq contract-caller ACCOUNT_OWNER) + (match (var-get account-owner) + owner (is-eq contract-caller owner) + false + ) ) ;; Check if caller is the authorized agent (define-private (is-agent) - (is-eq contract-caller ACCOUNT_AGENT) + (match (var-get account-agent) + agent (is-eq contract-caller agent) + false + ) ) ;; Check if contract type is valid @@ -481,19 +534,17 @@ ) ;; ============================================================ -;; INITIALIZATION +;; DEPLOYMENT ;; ============================================================ (begin - ;; Print creation event (print { - notification: "agent-account/created", + notification: "agent-account/deployed", payload: { - config: (get-configuration), - approvalTypes: (get-approval-types), - agentPermissions: (get-agent-permissions) + account: SELF, + deployer: DEPLOYER, + deployedBurnBlock: DEPLOYED_BURN_BLOCK, + deployedStacksBlock: DEPLOYED_STACKS_BLOCK } }) - ;; Auto-register with agent registry - (contract-call? .agent-registry register-agent-account ACCOUNT_OWNER ACCOUNT_AGENT) ) diff --git a/contracts/agent/agent-registry.clar b/contracts/agent/agent-registry.clar index 3e2273e..6237233 100644 --- a/contracts/agent/agent-registry.clar +++ b/contracts/agent/agent-registry.clar @@ -1,13 +1,16 @@ ;; title: agent-registry -;; version: 1.0.0 -;; summary: Registry for verified agent accounts with template hash verification. +;; version: 2.0.0 +;; summary: Registry for verified agent accounts with on-chain hash verification. ;; description: Tracks approved contract templates and registered agent accounts. -;; Designed to use contract-hash? when Clarity 4 is available. -;; Until then, provides manual attestation levels for trust. +;; Uses Clarity 4 contract-hash? to verify agent accounts match +;; approved templates during registration. Permissionless registration +;; gated by hash verification - same code, same hash, verified on-chain. ;; TRAITS (impl-trait .dao-traits.extension) +(use-trait agent-account-config-trait .agent-traits.agent-account-config) + ;; CONSTANTS ;; Error codes @@ -22,23 +25,25 @@ (define-constant ERR_OWNER_MUST_BE_STANDARD (err u2008)) (define-constant ERR_ACCOUNT_ALREADY_ACTIVE (err u2009)) (define-constant ERR_ACCOUNT_ALREADY_INACTIVE (err u2010)) +(define-constant ERR_TEMPLATE_NOT_APPROVED (err u2011)) +(define-constant ERR_HASH_NOT_AVAILABLE (err u2012)) ;; Attestation levels (define-constant ATTESTATION_UNVERIFIED u0) ;; Default, no verification (define-constant ATTESTATION_REGISTERED u1) ;; Registered but not hash-verified -(define-constant ATTESTATION_HASH_VERIFIED u2) ;; Hash matches approved template (future) +(define-constant ATTESTATION_HASH_VERIFIED u2) ;; Hash matches approved template (define-constant ATTESTATION_AUDITED u3) ;; Manually audited and approved (define-constant MAX_ATTESTATION_LEVEL u3) ;; Contract details (define-constant DEPLOYED_BURN_BLOCK burn-block-height) (define-constant DEPLOYED_STACKS_BLOCK stacks-block-height) -(define-constant SELF (as-contract tx-sender)) +;; Note: as-contract not available in Clarity 4 constants. +;; Contract principal is derived at runtime where needed. ;; DATA MAPS -;; Approved template hashes (for future contract-hash? verification) -;; When Clarity 4 is available, agent-accounts can be verified against these hashes +;; Approved template hashes for contract-hash? verification (define-map ApprovedTemplates (buff 32) { name: (string-ascii 64), version: uint, @@ -47,7 +52,6 @@ }) ;; Registered agent accounts -;; Stores ownership, agent address, verification status, and active state (define-map RegisteredAccounts principal { owner: principal, agent: principal, @@ -73,7 +77,6 @@ ;; ============================================================ ;; Add an approved template hash (DAO/extension only) -;; This is used to pre-approve contract templates before Clarity 4 (define-public (add-approved-template (hash (buff 32)) (name (string-ascii 64)) (version uint)) (begin (try! (is-dao-or-extension)) @@ -119,43 +122,50 @@ ) ;; ============================================================ -;; ACCOUNT REGISTRATION +;; ACCOUNT REGISTRATION (permissionless, hash-verified) ;; ============================================================ -;; Register an agent account (called by the agent-account contract on deploy) -;; The account is contract-caller, owner and agent are provided as params -(define-public (register-agent-account (owner principal) (agent principal)) +;; Register an agent account by passing the contract as a trait. +;; Verifies contract-hash? matches an approved template, then reads +;; owner/agent from get-config. Anyone can call - the hash IS the gate. +(define-public (register-agent-account (account )) (let ( - (account contract-caller) + (account-principal (contract-of account)) + (hash (unwrap! (contract-hash? account-principal) ERR_HASH_NOT_AVAILABLE)) + (config (try! (contract-call? account get-config))) + (owner (get owner config)) + (agent (get agent config)) ) - ;; Validate that account is a contract (has name component) - (try! (validate-is-contract account)) - ;; Validate that owner is a standard principal (no contract name) + ;; Verify hash matches an approved template + (asserts! (is-approved-template hash) ERR_TEMPLATE_NOT_APPROVED) + ;; Validate principals + (try! (validate-is-contract account-principal)) (try! (validate-is-standard-principal owner)) ;; Check not already registered (asserts! - (is-none (map-get? RegisteredAccounts account)) + (is-none (map-get? RegisteredAccounts account-principal)) ERR_ACCOUNT_ALREADY_REGISTERED ) - ;; Register the account with attestation level 1 (registered but not hash-verified) - (map-insert RegisteredAccounts account { + ;; Register with hash-verified attestation + (map-insert RegisteredAccounts account-principal { owner: owner, agent: agent, - template-hash: none, + template-hash: (some hash), registered-at: stacks-block-height, - attestation-level: ATTESTATION_REGISTERED, + attestation-level: ATTESTATION_HASH_VERIFIED, active: true }) ;; Set up lookup maps - (map-insert OwnerToAccount owner account) - (map-insert AgentToAccount agent account) + (map-insert OwnerToAccount owner account-principal) + (map-insert AgentToAccount agent account-principal) (print { notification: "agent-registry/register-agent-account", payload: { - account: account, + account: account-principal, owner: owner, agent: agent, - attestationLevel: ATTESTATION_REGISTERED, + templateHash: hash, + attestationLevel: ATTESTATION_HASH_VERIFIED, contractCaller: contract-caller, txSender: tx-sender } @@ -168,48 +178,36 @@ ;; VERIFICATION FUNCTIONS ;; ============================================================ -;; Verify an agent account against approved templates -;; TODO: When Clarity 4 is available, this will use contract-hash? -;; -;; Future implementation: -;; (define-public (verify-agent-account (account principal)) -;; (let ( -;; (hash (try! (contract-hash? account))) -;; (template (map-get? ApprovedTemplates hash)) -;; ) -;; (match template -;; t (if (get active t) -;; (begin -;; (try! (set-template-hash account hash)) -;; (try! (set-attestation-internal account ATTESTATION_HASH_VERIFIED)) -;; (ok true) -;; ) -;; (ok false) -;; ) -;; (ok false) -;; ) -;; ) -;; ) -;; -;; For now, returns false and requires manual attestation +;; Verify an already-registered account against approved templates. +;; Useful for upgrading attestation on accounts registered before +;; their template was approved, or re-verifying after template updates. (define-public (verify-agent-account (account principal)) - (begin - ;; Placeholder - contract-hash? not available in Clarity 3 - ;; In production Clarity 4, this would verify the contract hash + (let ( + (account-info (unwrap! (map-get? RegisteredAccounts account) ERR_ACCOUNT_NOT_FOUND)) + (hash (unwrap! (contract-hash? account) ERR_HASH_NOT_AVAILABLE)) + ) + (asserts! (is-approved-template hash) ERR_TEMPLATE_NOT_APPROVED) + (map-set RegisteredAccounts account (merge account-info { + template-hash: (some hash), + attestation-level: ATTESTATION_HASH_VERIFIED + })) (print { notification: "agent-registry/verify-agent-account", payload: { account: account, - result: "contract-hash-not-available", - message: "Clarity 4 required for hash verification" + templateHash: hash, + previousLevel: (get attestation-level account-info), + newLevel: ATTESTATION_HASH_VERIFIED, + contractCaller: contract-caller, + txSender: tx-sender } }) - (ok false) + (ok true) ) ) ;; Set attestation level manually (DAO/extension only) -;; Used for manual audits or when contract-hash? is not available +;; Used for manual audits or upgrading to AUDITED level (define-public (set-attestation-level (account principal) (level uint)) (let ((account-info (unwrap! (map-get? RegisteredAccounts account) ERR_ACCOUNT_NOT_FOUND))) (try! (is-dao-or-extension)) @@ -229,30 +227,6 @@ ) ) -;; Set template hash for an account (DAO/extension only) -;; This is for when we know the hash but can't use contract-hash? yet -(define-public (set-template-hash (account principal) (hash (buff 32))) - (let ((account-info (unwrap! (map-get? RegisteredAccounts account) ERR_ACCOUNT_NOT_FOUND))) - (try! (is-dao-or-extension)) - ;; Verify the template hash is approved - (asserts! - (is-approved-template hash) - ERR_TEMPLATE_NOT_FOUND - ) - (map-set RegisteredAccounts account (merge account-info { template-hash: (some hash) })) - (print { - notification: "agent-registry/set-template-hash", - payload: { - account: account, - templateHash: hash, - contractCaller: contract-caller, - txSender: tx-sender - } - }) - (ok true) - ) -) - ;; ============================================================ ;; AGENT ACTIVATION MANAGEMENT ;; ============================================================ @@ -365,7 +339,6 @@ ;; Get contract info (define-read-only (get-contract-info) { - self: SELF, deployedBurnBlock: DEPLOYED_BURN_BLOCK, deployedStacksBlock: DEPLOYED_STACKS_BLOCK, maxAttestationLevel: MAX_ATTESTATION_LEVEL diff --git a/deployments/default.simnet-plan.yaml b/deployments/default.simnet-plan.yaml index e004909..eb4d862 100644 --- a/deployments/default.simnet-plan.yaml +++ b/deployments/default.simnet-plan.yaml @@ -1,184 +1,186 @@ ---- id: 0 -name: "Simulated deployment, used as a default for `clarinet console`, `clarinet test` and `clarinet check`" +name: Simulated deployment, used as a default for `clarinet console`, `clarinet test` and `clarinet check` network: simnet genesis: wallets: - - name: deployer - address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - balance: "100000000000000" - sbtc-balance: "1000000000" - - name: faucet - address: STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6 - balance: "100000000000000" - sbtc-balance: "1000000000" - - name: wallet_1 - address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 - balance: "100000000000000" - sbtc-balance: "1000000000" - - name: wallet_2 - address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG - balance: "100000000000000" - sbtc-balance: "1000000000" - - name: wallet_3 - address: ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC - balance: "100000000000000" - sbtc-balance: "1000000000" - - name: wallet_4 - address: ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND - balance: "100000000000000" - sbtc-balance: "1000000000" - - name: wallet_5 - address: ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB - balance: "100000000000000" - sbtc-balance: "1000000000" - - name: wallet_6 - address: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0 - balance: "100000000000000" - sbtc-balance: "1000000000" - - name: wallet_7 - address: ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ - balance: "100000000000000" - sbtc-balance: "1000000000" - - name: wallet_8 - address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP - balance: "100000000000000" - sbtc-balance: "1000000000" + - name: deployer + address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: faucet + address: STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6 + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_1 + address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_2 + address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_3 + address: ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_4 + address: ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_5 + address: ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_6 + address: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0 + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_7 + address: ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_8 + address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP + balance: '100000000000000' + sbtc-balance: '1000000000' contracts: - - genesis - - lockup - - bns - - cost-voting - - costs - - pox - - costs-2 - - pox-2 - - costs-3 - - pox-3 - - pox-4 - - signers - - signers-voting - - costs-4 + - genesis + - lockup + - bns + - cost-voting + - costs + - pox + - costs-2 + - pox-2 + - costs-3 + - pox-3 + - pox-4 + - signers + - signers-voting + - costs-4 plan: batches: - - id: 0 - transactions: - - emulated-contract-publish: - contract-name: sip-010-trait-ft-standard - emulated-sender: SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE - path: "./.cache/requirements/SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.clar" - clarity-version: 1 - epoch: "2.0" - - id: 1 - transactions: - - emulated-contract-publish: - contract-name: sbtc-registry - emulated-sender: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4 - path: "./.cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar" - clarity-version: 3 - - emulated-contract-publish: - contract-name: sbtc-token - emulated-sender: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4 - path: "./.cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar" - clarity-version: 3 - - emulated-contract-publish: - contract-name: sbtc-deposit - emulated-sender: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4 - path: "./.cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-deposit.clar" - clarity-version: 3 - - emulated-contract-publish: - contract-name: dao-traits - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/traits/dao-traits.clar - clarity-version: 3 - - emulated-contract-publish: - contract-name: base-dao - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/dao/base-dao.clar - clarity-version: 3 - - emulated-contract-publish: - contract-name: agent-registry - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/agent/agent-registry.clar - clarity-version: 3 - - emulated-contract-publish: - contract-name: agent-traits - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/traits/agent-traits.clar - clarity-version: 3 - - emulated-contract-publish: - contract-name: agent-account - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/agent/agent-account.clar - clarity-version: 3 - - emulated-contract-publish: - contract-name: checkin-registry - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/checkin-registry.clar - clarity-version: 3 - - emulated-contract-publish: - contract-name: mock-sbtc - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/token/mock-sbtc.clar - clarity-version: 3 - - emulated-contract-publish: - contract-name: dao-token - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/token/dao-token.clar - clarity-version: 3 - - emulated-contract-publish: - contract-name: core-proposals - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/extensions/core-proposals.clar - clarity-version: 3 - - emulated-contract-publish: - contract-name: dao-charter - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/extensions/dao-charter.clar - clarity-version: 3 - - emulated-contract-publish: - contract-name: dao-epoch - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/extensions/dao-epoch.clar - clarity-version: 3 - - emulated-contract-publish: - contract-name: dao-run-cost - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/core/dao-run-cost.clar - clarity-version: 3 - - emulated-contract-publish: - contract-name: dao-token-owner - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/extensions/dao-token-owner.clar - clarity-version: 3 - - emulated-contract-publish: - contract-name: dao-treasury - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/extensions/dao-treasury.clar - clarity-version: 3 - - emulated-contract-publish: - contract-name: init-proposal - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/proposals/init-proposal.clar - clarity-version: 3 - - emulated-contract-publish: - contract-name: proof-registry - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/proof-registry.clar - clarity-version: 3 - - emulated-contract-publish: - contract-name: manifesto - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/manifesto.clar - clarity-version: 3 - - emulated-contract-publish: - contract-name: sbtc-config - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/config/sbtc-config.clar - clarity-version: 3 - - emulated-contract-publish: - contract-name: test-proposal - emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - path: contracts/proposals/test-proposal.clar - clarity-version: 3 - epoch: "3.0" + - id: 0 + transactions: + - transaction-type: emulated-contract-publish + contract-name: sip-010-trait-ft-standard + emulated-sender: SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE + path: .cache/requirements/SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.clar + clarity-version: 1 + epoch: '2.0' + - id: 1 + transactions: + - transaction-type: emulated-contract-publish + contract-name: sbtc-registry + emulated-sender: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4 + path: .cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: sbtc-token + emulated-sender: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4 + path: .cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: sbtc-deposit + emulated-sender: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4 + path: .cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-deposit.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: dao-traits + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/traits/dao-traits.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: agent-traits + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/traits/agent-traits.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: agent-account + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/agent/agent-account.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: base-dao + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/dao/base-dao.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: checkin-registry + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/checkin-registry.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: mock-sbtc + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/token/mock-sbtc.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: dao-token + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/token/dao-token.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: core-proposals + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/extensions/core-proposals.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: dao-charter + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/extensions/dao-charter.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: dao-epoch + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/extensions/dao-epoch.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: dao-run-cost + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/core/dao-run-cost.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: dao-token-owner + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/extensions/dao-token-owner.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: dao-treasury + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/extensions/dao-treasury.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: init-proposal + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/proposals/init-proposal.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: proof-registry + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/proof-registry.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: manifesto + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/manifesto.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: sbtc-config + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/config/sbtc-config.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: test-proposal + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/proposals/test-proposal.clar + clarity-version: 3 + epoch: '3.0' + - id: 2 + transactions: + - transaction-type: emulated-contract-publish + contract-name: agent-registry + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/agent/agent-registry.clar + clarity-version: 4 + epoch: '3.3' diff --git a/package-lock.json b/package-lock.json index 9b8f58d..4d62665 100644 --- a/package-lock.json +++ b/package-lock.json @@ -789,12 +789,12 @@ ] }, "node_modules/@stacks/clarinet-sdk": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@stacks/clarinet-sdk/-/clarinet-sdk-3.13.1.tgz", - "integrity": "sha512-n3ehFVHcJ4P7vpDHbZRiqUHrzoNfY+P5ZRgpXiISJrQvinNGbX+SAtH3E+aw2L6fXqRyZQB0v/w9XVV8yjtXsg==", + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/@stacks/clarinet-sdk/-/clarinet-sdk-3.14.1.tgz", + "integrity": "sha512-fCrUNWDyXNoOCGgEdGLJxNNZHafPamXLRr+3CaTAeJ+LAWNSH7uo4iEDzQs/Q48D0YWu9rloznyBVSb57pNgGg==", "license": "GPL-3.0", "dependencies": { - "@stacks/clarinet-sdk-wasm": "3.13.1", + "@stacks/clarinet-sdk-wasm": "3.14.1", "@stacks/transactions": "^7.0.6", "kolorist": "^1.8.0", "prompts": "^2.4.2", @@ -805,9 +805,9 @@ } }, "node_modules/@stacks/clarinet-sdk-wasm": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@stacks/clarinet-sdk-wasm/-/clarinet-sdk-wasm-3.13.1.tgz", - "integrity": "sha512-bNGdmhAsGhMmqbJ6DnBdZBKGQAMAyQXAEkoTjHLFjxj013XxiUMXEl1nVxdQmjqDuZFXn9bkkHTF56YF1aCQ/g==", + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/@stacks/clarinet-sdk-wasm/-/clarinet-sdk-wasm-3.14.1.tgz", + "integrity": "sha512-hoDZcEKvp56cOWc+D5CZrPKBqZNC5fJ+7lG4psN6SSW2CtuFUcAqKPSFx6TJjt/WGIfejWvB3N9bWRZfyji7yw==", "license": "GPL-3.0" }, "node_modules/@stacks/common": { diff --git a/tests/agent-account.test.ts b/tests/agent-account.test.ts index eeec2ad..966edd3 100644 --- a/tests/agent-account.test.ts +++ b/tests/agent-account.test.ts @@ -17,9 +17,9 @@ const daoTokenAddress = `${deployer}.dao-token`; const testProposalAddress = `${deployer}.test-proposal`; // The agent-account uses deployer as owner and wallet_2 as agent -// (hardcoded in the contract constants) -const accountOwner = deployer; // ACCOUNT_OWNER = ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM (deployer) -const accountAgent = wallet2; // ACCOUNT_AGENT = ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG (wallet_2) +// (set via initialize after deployment) +const accountOwner = deployer; +const accountAgent = wallet2; const unauthorized = wallet3; // Error codes @@ -28,6 +28,8 @@ const ERR_OPERATION_NOT_ALLOWED = 4001; const ERR_CONTRACT_NOT_APPROVED = 4002; const ERR_INVALID_APPROVAL_TYPE = 4003; const ERR_ZERO_AMOUNT = 4004; +const ERR_ALREADY_INITIALIZED = 4005; +const ERR_NOT_INITIALIZED = 4006; // Permission flags const PERMISSION_MANAGE_ASSETS = 1; // pow(2, 0) @@ -41,9 +43,127 @@ const APPROVED_CONTRACT_VOTING = 1; const APPROVED_CONTRACT_SWAP = 2; const APPROVED_CONTRACT_TOKEN = 3; +// Helper: initialize the agent account with deployer as owner, wallet2 as agent +function initializeAgentAccount() { + const receipt = simnet.callPublicFn( + agentAccountAddress, + "initialize", + [Cl.principal(accountOwner), Cl.principal(accountAgent)], + deployer + ); + expect(receipt.result).toBeOk(Cl.bool(true)); +} + +describe("agent-account: initialization", function () { + it("initialize() succeeds for deployer", function () { + // arrange + // act + const receipt = simnet.callPublicFn( + agentAccountAddress, + "initialize", + [Cl.principal(accountOwner), Cl.principal(accountAgent)], + deployer + ); + // assert + expect(receipt.result).toBeOk(Cl.bool(true)); + }); + + it("initialize() fails for non-deployer", function () { + // arrange + // act + const receipt = simnet.callPublicFn( + agentAccountAddress, + "initialize", + [Cl.principal(accountOwner), Cl.principal(accountAgent)], + wallet1 + ); + // assert + expect(receipt.result).toBeErr(Cl.uint(ERR_CALLER_NOT_OWNER)); + }); + + it("initialize() fails if called twice", function () { + // arrange + initializeAgentAccount(); + // act + const receipt = simnet.callPublicFn( + agentAccountAddress, + "initialize", + [Cl.principal(accountOwner), Cl.principal(accountAgent)], + deployer + ); + // assert + expect(receipt.result).toBeErr(Cl.uint(ERR_ALREADY_INITIALIZED)); + }); + + it("operations fail before initialization", function () { + // arrange + // act + const receipt = simnet.callPublicFn( + agentAccountAddress, + "deposit-stx", + [Cl.uint(1000000)], + accountOwner + ); + // assert + expect(receipt.result).toBeErr(Cl.uint(ERR_NOT_INITIALIZED)); + }); + + it("get-config() returns error before initialization", function () { + // arrange + // act + const receipt = simnet.callPublicFn( + agentAccountAddress, + "get-config", + [], + deployer + ); + // assert + expect(receipt.result).toBeErr(Cl.uint(ERR_NOT_INITIALIZED)); + }); + + it("get-config() returns ok after initialization", function () { + // arrange + initializeAgentAccount(); + // act + const receipt = simnet.callPublicFn( + agentAccountAddress, + "get-config", + [], + deployer + ); + // assert + expect(receipt.result.type).toBe(ClarityType.ResponseOk); + }); +}); + describe("agent-account: initial state", function () { - it("get-configuration() returns valid contract configuration", function () { + it("get-configuration() returns valid contract configuration before init", function () { + // arrange + // act + const result = simnet.callReadOnlyFn( + agentAccountAddress, + "get-configuration", + [], + deployer + ).result; + // assert + expect(result.type).toBe(ClarityType.Tuple); + expect(result).toStrictEqual( + Cl.tuple({ + account: Cl.principal(agentAccountAddress), + owner: Cl.none(), + agent: Cl.none(), + initialized: Cl.bool(false), + deployer: Cl.principal(deployer), + deployedBurnBlock: Cl.uint(3), + deployedStacksBlock: Cl.uint(3), + }) + ); + }); + + it("get-configuration() returns principals after initialization", function () { // arrange + initializeAgentAccount(); // act const result = simnet.callReadOnlyFn( agentAccountAddress, @@ -53,6 +173,17 @@ describe("agent-account: initial state", function () { ).result; // assert expect(result.type).toBe(ClarityType.Tuple); + expect(result).toStrictEqual( + Cl.tuple({ + account: Cl.principal(agentAccountAddress), + owner: Cl.some(Cl.principal(accountOwner)), + agent: Cl.some(Cl.principal(accountAgent)), + initialized: Cl.bool(true), + deployer: Cl.principal(deployer), + deployedBurnBlock: Cl.uint(3), + deployedStacksBlock: Cl.uint(3), + }) + ); }); it("get-agent-permissions() returns default permissions", function () { @@ -128,37 +259,12 @@ describe("agent-account: initial state", function () { // assert expect(result).toStrictEqual(Cl.bool(false)); }); - - it("auto-registered with agent-registry on deploy", function () { - // arrange - // act - const result = simnet.callReadOnlyFn( - agentRegistryAddress, - "is-registered-account", - [Cl.principal(agentAccountAddress)], - deployer - ).result; - // assert - expect(result).toStrictEqual(Cl.bool(true)); - }); - - it("agent-registry records correct owner and agent", function () { - // arrange - // act - const result = simnet.callReadOnlyFn( - agentRegistryAddress, - "get-account-info", - [Cl.principal(agentAccountAddress)], - deployer - ).result; - // assert - expect(result.type).toBe(ClarityType.OptionalSome); - }); }); describe("agent-account: STX deposit/withdraw", function () { it("deposit-stx() succeeds for owner", function () { // arrange + initializeAgentAccount(); const amount = 1000000; // 1 STX // act const receipt = simnet.callPublicFn( @@ -176,6 +282,7 @@ describe("agent-account: STX deposit/withdraw", function () { it("deposit-stx() succeeds for agent with permission", function () { // arrange + initializeAgentAccount(); const amount = 500000; // act const receipt = simnet.callPublicFn( @@ -190,6 +297,7 @@ describe("agent-account: STX deposit/withdraw", function () { it("deposit-stx() fails for unauthorized caller", function () { // arrange + initializeAgentAccount(); const amount = 100000; // act const receipt = simnet.callPublicFn( @@ -204,6 +312,7 @@ describe("agent-account: STX deposit/withdraw", function () { it("deposit-stx() fails for zero amount", function () { // arrange + initializeAgentAccount(); // act const receipt = simnet.callPublicFn( agentAccountAddress, @@ -217,6 +326,7 @@ describe("agent-account: STX deposit/withdraw", function () { it("withdraw-stx() succeeds for owner and sends to owner", function () { // arrange + initializeAgentAccount(); const depositAmount = 2000000; const withdrawAmount = 1000000; simnet.callPublicFn( @@ -241,6 +351,7 @@ describe("agent-account: STX deposit/withdraw", function () { it("withdraw-stx() succeeds for agent with permission", function () { // arrange + initializeAgentAccount(); const depositAmount = 2000000; const withdrawAmount = 500000; simnet.callPublicFn( @@ -262,6 +373,7 @@ describe("agent-account: STX deposit/withdraw", function () { it("withdraw-stx() fails for unauthorized caller", function () { // arrange + initializeAgentAccount(); const amount = 100000; // act const receipt = simnet.callPublicFn( @@ -278,6 +390,7 @@ describe("agent-account: STX deposit/withdraw", function () { describe("agent-account: FT deposit/withdraw", function () { it("deposit-ft() succeeds for owner", function () { // arrange + initializeAgentAccount(); const amount = 1000; // First mint some mock-sbtc to owner simnet.callPublicFn( @@ -299,6 +412,7 @@ describe("agent-account: FT deposit/withdraw", function () { it("deposit-ft() succeeds for agent with permission", function () { // arrange + initializeAgentAccount(); const amount = 500; simnet.callPublicFn( mockSbtcAddress, @@ -319,6 +433,7 @@ describe("agent-account: FT deposit/withdraw", function () { it("deposit-ft() fails for unauthorized caller", function () { // arrange + initializeAgentAccount(); const amount = 100; simnet.callPublicFn( mockSbtcAddress, @@ -339,6 +454,7 @@ describe("agent-account: FT deposit/withdraw", function () { it("withdraw-ft() requires approved token contract", function () { // arrange + initializeAgentAccount(); const amount = 100; simnet.callPublicFn( mockSbtcAddress, @@ -365,6 +481,7 @@ describe("agent-account: FT deposit/withdraw", function () { it("withdraw-ft() succeeds after token contract is approved", function () { // arrange + initializeAgentAccount(); const amount = 100; simnet.callPublicFn( mockSbtcAddress, @@ -400,6 +517,7 @@ describe("agent-account: FT deposit/withdraw", function () { describe("agent-account: contract approval/revocation", function () { it("approve-contract() succeeds for owner", function () { // arrange + initializeAgentAccount(); // act const receipt = simnet.callPublicFn( agentAccountAddress, @@ -421,6 +539,7 @@ describe("agent-account: contract approval/revocation", function () { it("approve-contract() succeeds for agent with permission", function () { // arrange + initializeAgentAccount(); // act const receipt = simnet.callPublicFn( agentAccountAddress, @@ -434,6 +553,7 @@ describe("agent-account: contract approval/revocation", function () { it("approve-contract() fails for unauthorized caller", function () { // arrange + initializeAgentAccount(); // act const receipt = simnet.callPublicFn( agentAccountAddress, @@ -447,6 +567,7 @@ describe("agent-account: contract approval/revocation", function () { it("approve-contract() fails for invalid type", function () { // arrange + initializeAgentAccount(); // act const receipt = simnet.callPublicFn( agentAccountAddress, @@ -460,6 +581,7 @@ describe("agent-account: contract approval/revocation", function () { it("revoke-contract() removes approval", function () { // arrange + initializeAgentAccount(); simnet.callPublicFn( agentAccountAddress, "approve-contract", @@ -489,6 +611,7 @@ describe("agent-account: contract approval/revocation", function () { describe("agent-account: permission management", function () { it("set-agent-can-manage-assets() succeeds for owner", function () { // arrange + initializeAgentAccount(); // act - disable manage assets const receipt = simnet.callPublicFn( agentAccountAddress, @@ -518,6 +641,7 @@ describe("agent-account: permission management", function () { it("set-agent-can-manage-assets() fails for non-owner", function () { // arrange + initializeAgentAccount(); // act const receipt = simnet.callPublicFn( agentAccountAddress, @@ -531,6 +655,7 @@ describe("agent-account: permission management", function () { it("set-agent-can-use-proposals() succeeds for owner", function () { // arrange + initializeAgentAccount(); // act const receipt = simnet.callPublicFn( agentAccountAddress, @@ -559,6 +684,7 @@ describe("agent-account: permission management", function () { it("set-agent-can-approve-revoke-contracts() succeeds for owner", function () { // arrange + initializeAgentAccount(); // act const receipt = simnet.callPublicFn( agentAccountAddress, @@ -587,6 +713,7 @@ describe("agent-account: permission management", function () { it("set-agent-can-buy-sell-assets() succeeds for owner", function () { // arrange + initializeAgentAccount(); // act - enable buy/sell (disabled by default) const receipt = simnet.callPublicFn( agentAccountAddress, @@ -615,6 +742,7 @@ describe("agent-account: permission management", function () { it("agent cannot deposit after permission is revoked", function () { // arrange + initializeAgentAccount(); simnet.callPublicFn( agentAccountAddress, "set-agent-can-manage-assets", @@ -634,6 +762,7 @@ describe("agent-account: permission management", function () { it("agent can deposit after permission is re-enabled", function () { // arrange + initializeAgentAccount(); simnet.callPublicFn( agentAccountAddress, "set-agent-can-manage-assets", @@ -659,6 +788,7 @@ describe("agent-account: permission management", function () { it("owner can always deposit regardless of agent permissions", function () { // arrange - disable agent manage assets + initializeAgentAccount(); simnet.callPublicFn( agentAccountAddress, "set-agent-can-manage-assets", @@ -678,8 +808,9 @@ describe("agent-account: permission management", function () { }); describe("agent-account: get-config trait implementation", function () { - it("get-config() returns ok response", function () { + it("get-config() returns ok response after initialization", function () { // arrange + initializeAgentAccount(); // act const receipt = simnet.callPublicFn( agentAccountAddress, @@ -695,6 +826,7 @@ describe("agent-account: get-config trait implementation", function () { describe("agent-account: proposal interaction", function () { it("create-proposal() fails without approved voting contract", function () { // arrange + initializeAgentAccount(); // act const receipt = simnet.callPublicFn( agentAccountAddress, @@ -712,6 +844,7 @@ describe("agent-account: proposal interaction", function () { it("vote-on-proposal() fails without approved voting contract", function () { // arrange + initializeAgentAccount(); // act const receipt = simnet.callPublicFn( agentAccountAddress, @@ -729,6 +862,7 @@ describe("agent-account: proposal interaction", function () { it("conclude-proposal() fails without approved voting contract", function () { // arrange + initializeAgentAccount(); // act const receipt = simnet.callPublicFn( agentAccountAddress, @@ -746,6 +880,7 @@ describe("agent-account: proposal interaction", function () { it("agent cannot use proposals after permission is revoked", function () { // arrange + initializeAgentAccount(); simnet.callPublicFn( agentAccountAddress, "approve-contract", @@ -816,6 +951,7 @@ describe("agent-account: bit operations", function () { it("enabling buy-sell adds bit correctly", function () { // arrange + initializeAgentAccount(); simnet.callPublicFn( agentAccountAddress, "set-agent-can-buy-sell-assets", @@ -843,6 +979,7 @@ describe("agent-account: bit operations", function () { it("disabling all permissions results in 0", function () { // arrange + initializeAgentAccount(); simnet.callPublicFn( agentAccountAddress, "set-agent-can-manage-assets", @@ -894,6 +1031,8 @@ describe("agent-account: error codes documentation", function () { expect(ERR_CONTRACT_NOT_APPROVED).toBe(4002); expect(ERR_INVALID_APPROVAL_TYPE).toBe(4003); expect(ERR_ZERO_AMOUNT).toBe(4004); + expect(ERR_ALREADY_INITIALIZED).toBe(4005); + expect(ERR_NOT_INITIALIZED).toBe(4006); }); it("documents permission flags", function () { diff --git a/tests/agent-registry.test.ts b/tests/agent-registry.test.ts index c9ad8c8..aaadc52 100644 --- a/tests/agent-registry.test.ts +++ b/tests/agent-registry.test.ts @@ -11,6 +11,7 @@ const wallet3 = accounts.get("wallet_3")!; // contract info const agentRegistryAddress = `${deployer}.agent-registry`; const baseDaoAddress = `${deployer}.base-dao`; +const agentAccountAddress = `${deployer}.agent-account`; // Error codes const ERR_NOT_DAO_OR_EXTENSION = 2000; @@ -24,6 +25,8 @@ const ERR_ACCOUNT_IS_NOT_CONTRACT = 2007; const ERR_OWNER_MUST_BE_STANDARD = 2008; const ERR_ACCOUNT_ALREADY_ACTIVE = 2009; const ERR_ACCOUNT_ALREADY_INACTIVE = 2010; +const ERR_TEMPLATE_NOT_APPROVED = 2011; +const ERR_HASH_NOT_AVAILABLE = 2012; // Attestation levels const ATTESTATION_UNVERIFIED = 0; @@ -47,6 +50,8 @@ describe("agent-registry: initial state", function () { ).result; // assert expect(result.type).toBe(ClarityType.Tuple); + // contract info has deployedBurnBlock, deployedStacksBlock, maxAttestationLevel + // no "self" field in v2 }); it("is-approved-template() returns false for unknown hash", function () { @@ -159,32 +164,39 @@ describe("agent-registry: template management authorization", function () { }); describe("agent-registry: account registration", function () { - it("register-agent-account() fails when called by standard principal", function () { + // NOTE: Full happy-path registration requires: + // 1. DAO constructed (base-dao initialized) + // 2. Template hash approved via DAO proposal + // 3. agent-account contract deployed and initialized with get-config + // These unit tests verify error paths without full DAO setup. + + it("register-agent-account() fails when agent-account is not initialized", function () { // arrange - // act - wallet1 is a standard principal, not a contract + // act - pass agent-account as trait parameter + // agent-account's get-config will return an error since it's not initialized const receipt = simnet.callPublicFn( agentRegistryAddress, "register-agent-account", - [Cl.principal(wallet1), Cl.principal(wallet2)], + [Cl.principal(agentAccountAddress)], wallet1 ); - // assert - should fail because contract-caller is not a contract - expect(receipt.result).toBeErr(Cl.uint(ERR_ACCOUNT_IS_NOT_CONTRACT)); + // assert - fails because contract-hash? or get-config returns an error + // before reaching any principal validation + expect(receipt.result.type).toBe(ClarityType.ResponseErr); }); - it("register-agent-account() fails when owner is a contract", function () { + it("register-agent-account() fails regardless of caller (permissionless but hash-gated)", function () { // arrange - // act - try to register with owner being a contract principal - // We simulate this by calling directly (which would fail on first check anyway) - // But the validation order matters - it checks contract-caller first + // act - even deployer can't register without proper setup const receipt = simnet.callPublicFn( agentRegistryAddress, "register-agent-account", - [Cl.principal(agentRegistryAddress), Cl.principal(wallet2)], - wallet1 + [Cl.principal(agentAccountAddress)], + deployer ); - // assert - fails on the first check (caller not a contract) - expect(receipt.result).toBeErr(Cl.uint(ERR_ACCOUNT_IS_NOT_CONTRACT)); + // assert - same error as above, registration is permissionless + // but gated by hash verification and get-config availability + expect(receipt.result.type).toBe(ClarityType.ResponseErr); }); }); @@ -216,33 +228,19 @@ describe("agent-registry: attestation level management", function () { }); }); -describe("agent-registry: template hash management", function () { - it("set-template-hash() fails for unregistered account", function () { - // arrange - // act - const receipt = simnet.callPublicFn( - agentRegistryAddress, - "set-template-hash", - [Cl.principal(agentRegistryAddress), Cl.buffer(sampleHash)], - wallet1 - ); - // assert - expect(receipt.result).toBeErr(Cl.uint(ERR_ACCOUNT_NOT_FOUND)); - }); -}); - describe("agent-registry: verification function", function () { - it("verify-agent-account() returns false (Clarity 4 not available)", function () { + it("verify-agent-account() returns ERR_ACCOUNT_NOT_FOUND for unregistered account", function () { // arrange - // act + // act - verify-agent-account now does real Clarity 4 hash verification + // but first checks if the account is registered const receipt = simnet.callPublicFn( agentRegistryAddress, "verify-agent-account", [Cl.principal(agentRegistryAddress)], wallet1 ); - // assert - returns ok(false) because contract-hash? is not available - expect(receipt.result).toBeOk(Cl.bool(false)); + // assert - fails because account is not registered + expect(receipt.result).toBeErr(Cl.uint(ERR_ACCOUNT_NOT_FOUND)); }); }); @@ -301,7 +299,6 @@ describe("agent-registry: read-only functions", function () { it("get-account-by-agent() returns none for unknown agent", function () { // arrange - // Note: wallet2 is used by agent-account as its agent, so use wallet3 here // act const result = simnet.callReadOnlyFn( agentRegistryAddress, @@ -354,26 +351,6 @@ describe("agent-registry: authorization patterns", function () { expect(result1.result).toBeErr(Cl.uint(ERR_ACCOUNT_NOT_FOUND)); expect(result2.result).toBeErr(Cl.uint(ERR_ACCOUNT_NOT_FOUND)); }); - - it("only DAO or extensions can set template hashes", function () { - // arrange - // act - const result1 = simnet.callPublicFn( - agentRegistryAddress, - "set-template-hash", - [Cl.principal(agentRegistryAddress), Cl.buffer(sampleHash)], - wallet1 - ); - const result2 = simnet.callPublicFn( - agentRegistryAddress, - "set-template-hash", - [Cl.principal(agentRegistryAddress), Cl.buffer(sampleHash)], - wallet2 - ); - // assert - expect(result1.result).toBeErr(Cl.uint(ERR_ACCOUNT_NOT_FOUND)); - expect(result2.result).toBeErr(Cl.uint(ERR_ACCOUNT_NOT_FOUND)); - }); }); describe("agent-registry: deactivate-agent authorization", function () { @@ -473,6 +450,8 @@ describe("agent-registry: error codes documentation", function () { expect(ERR_OWNER_MUST_BE_STANDARD).toBe(2008); expect(ERR_ACCOUNT_ALREADY_ACTIVE).toBe(2009); expect(ERR_ACCOUNT_ALREADY_INACTIVE).toBe(2010); + expect(ERR_TEMPLATE_NOT_APPROVED).toBe(2011); + expect(ERR_HASH_NOT_AVAILABLE).toBe(2012); }); it("documents attestation levels", function () { diff --git a/tests/integration/agent-workflow.test.ts b/tests/integration/agent-workflow.test.ts index a55136b..6922604 100644 --- a/tests/integration/agent-workflow.test.ts +++ b/tests/integration/agent-workflow.test.ts @@ -19,7 +19,7 @@ const mockSbtcAddress = `${deployer}.mock-sbtc`; const daoTokenAddress = `${deployer}.dao-token`; // The agent-account uses deployer as owner and wallet_2 as agent -// (hardcoded in the contract constants) +// (set via initialize) const accountOwner = deployer; const accountAgent = wallet2; const unauthorized = wallet3; @@ -37,6 +37,19 @@ const APPROVED_CONTRACT_TOKEN = 3; const ERR_CALLER_NOT_OWNER = 4000; const ERR_OPERATION_NOT_ALLOWED = 4001; const ERR_CONTRACT_NOT_APPROVED = 4002; +const ERR_NOT_INITIALIZED = 4006; + +// Helper to initialize agent-account (must be called in every test that +// interacts with agent-account public functions, since each test gets a +// fresh simnet) +function initializeAgentAccount() { + return simnet.callPublicFn( + agentAccountAddress, + "initialize", + [Cl.principal(accountOwner), Cl.principal(accountAgent)], + deployer + ); +} // Helper to mint mock sBTC function mintMockSbtc(amount: number, recipient: string) { @@ -74,60 +87,70 @@ function constructDao() { ); } -describe("agent-workflow: agent-account registration", function () { - it("agent-account auto-registers with agent-registry on deploy", function () { - // arrange & act - contracts are deployed by simnet +describe("agent-workflow: agent-account initialization", function () { + it("initialize() succeeds for deployer", function () { + // arrange & act + const receipt = initializeAgentAccount(); - // assert - agent-account is registered - const isRegistered = simnet.callReadOnlyFn( - agentRegistryAddress, - "is-registered-account", - [Cl.principal(agentAccountAddress)], - deployer - ).result; - expect(isRegistered).toStrictEqual(Cl.bool(true)); + // assert + expect(receipt.result).toBeOk(Cl.bool(true)); }); - it("agent-registry records correct account info", function () { - // arrange & act + it("get-config() works after initialization", function () { + // arrange + initializeAgentAccount(); - // assert - get account info - const accountInfo = simnet.callReadOnlyFn( - agentRegistryAddress, - "get-account-info", - [Cl.principal(agentAccountAddress)], + // act + const configReceipt = simnet.callPublicFn( + agentAccountAddress, + "get-config", + [], deployer - ).result; + ); - expect(accountInfo.type).toBe(ClarityType.OptionalSome); - if (accountInfo.type === ClarityType.OptionalSome && accountInfo.value.type === ClarityType.Tuple) { - expect(accountInfo.value.value.owner).toStrictEqual(Cl.principal(accountOwner)); - expect(accountInfo.value.value.agent).toStrictEqual(Cl.principal(accountAgent)); - } + // assert + expect(configReceipt.result).toBeOk( + Cl.tuple({ + account: Cl.principal(agentAccountAddress), + agent: Cl.principal(accountAgent), + owner: Cl.principal(accountOwner), + "agent-can-manage-assets": Cl.bool(true), + "agent-can-use-proposals": Cl.bool(true), + }) + ); }); - it("agent can be looked up by owner in registry", function () { - // arrange & act - - // assert - lookup by owner - const account = simnet.callReadOnlyFn( - agentRegistryAddress, - "get-account-by-owner", - [Cl.principal(accountOwner)], + it("get-config() fails before initialization", function () { + // arrange & act - do NOT call initializeAgentAccount + const configReceipt = simnet.callPublicFn( + agentAccountAddress, + "get-config", + [], deployer - ).result; + ); - // Should return the agent-account - expect(account.type).toBe(ClarityType.OptionalSome); - if (account.type === ClarityType.OptionalSome) { - expect(account.value).toStrictEqual(Cl.principal(agentAccountAddress)); - } + // assert + expect(configReceipt.result).toBeErr(Cl.uint(ERR_NOT_INITIALIZED)); + }); + + it("public functions return ERR_NOT_INITIALIZED before initialization", function () { + // arrange & act - do NOT call initializeAgentAccount + const depositReceipt = simnet.callPublicFn( + agentAccountAddress, + "deposit-stx", + [Cl.uint(1000)], + accountOwner + ); + + // assert + expect(depositReceipt.result).toBeErr(Cl.uint(ERR_NOT_INITIALIZED)); }); }); describe("agent-workflow: token deposit flow", function () { it("owner deposits tokens to agent-account", function () { // arrange + initializeAgentAccount(); const depositAmount = 1000000; mintMockSbtc(depositAmount, accountOwner); @@ -145,6 +168,7 @@ describe("agent-workflow: token deposit flow", function () { it("agent deposits tokens with manage-assets permission", function () { // arrange + initializeAgentAccount(); const depositAmount = 500000; mintMockSbtc(depositAmount, accountAgent); @@ -162,6 +186,7 @@ describe("agent-workflow: token deposit flow", function () { it("unauthorized user cannot deposit", function () { // arrange + initializeAgentAccount(); const depositAmount = 100000; mintMockSbtc(depositAmount, unauthorized); @@ -180,7 +205,8 @@ describe("agent-workflow: token deposit flow", function () { describe("agent-workflow: proposal creation via agent-account", function () { it("agent creates proposal after voting contract approval", function () { - // arrange - construct DAO and approve voting contract + // arrange - initialize, construct DAO, and approve voting contract + initializeAgentAccount(); constructDao(); simnet.callPublicFn( agentAccountAddress, @@ -229,7 +255,8 @@ describe("agent-workflow: proposal creation via agent-account", function () { }); it("agent cannot create proposal without approved voting contract", function () { - // arrange - construct DAO but do NOT approve voting contract + // arrange - initialize, construct DAO but do NOT approve voting contract + initializeAgentAccount(); constructDao(); // act - agent tries to create proposal @@ -252,6 +279,8 @@ describe("agent-workflow: proposal creation via agent-account", function () { describe("agent-workflow: permission management", function () { it("owner can revoke agent manage-assets permission", function () { // arrange + initializeAgentAccount(); + // act - revoke manage-assets const receipt = simnet.callPublicFn( agentAccountAddress, @@ -276,7 +305,8 @@ describe("agent-workflow: permission management", function () { }); it("agent cannot deposit after manage-assets permission revoked", function () { - // arrange - revoke permission + // arrange - initialize and revoke permission + initializeAgentAccount(); simnet.callPublicFn( agentAccountAddress, "set-agent-can-manage-assets", @@ -299,7 +329,8 @@ describe("agent-workflow: permission management", function () { }); it("owner can still deposit after agent permission revoked", function () { - // arrange - revoke agent permission + // arrange - initialize and revoke agent permission + initializeAgentAccount(); simnet.callPublicFn( agentAccountAddress, "set-agent-can-manage-assets", @@ -322,7 +353,8 @@ describe("agent-workflow: permission management", function () { }); it("owner can revoke and re-enable permissions", function () { - // arrange - revoke permission + // arrange - initialize and revoke permission + initializeAgentAccount(); simnet.callPublicFn( agentAccountAddress, "set-agent-can-use-proposals", @@ -362,7 +394,8 @@ describe("agent-workflow: permission management", function () { }); it("agent cannot modify permissions (owner only)", function () { - // arrange & act - agent tries to modify permissions + // arrange & act - initialize, then agent tries to modify permissions + initializeAgentAccount(); const receipt = simnet.callPublicFn( agentAccountAddress, "set-agent-can-manage-assets", @@ -377,7 +410,10 @@ describe("agent-workflow: permission management", function () { describe("agent-workflow: contract approval flow", function () { it("owner approves and revokes voting contract", function () { - // arrange & act - approve + // arrange + initializeAgentAccount(); + + // act - approve const approveReceipt = simnet.callPublicFn( agentAccountAddress, "approve-contract", @@ -415,7 +451,10 @@ describe("agent-workflow: contract approval flow", function () { }); it("agent can approve contracts with permission", function () { - // arrange & act - agent approves token contract + // arrange + initializeAgentAccount(); + + // act - agent approves token contract const approveReceipt = simnet.callPublicFn( agentAccountAddress, "approve-contract", @@ -437,7 +476,8 @@ describe("agent-workflow: contract approval flow", function () { }); it("agent cannot approve contracts after permission revoked", function () { - // arrange - revoke approve/revoke permission + // arrange - initialize and revoke approve/revoke permission + initializeAgentAccount(); simnet.callPublicFn( agentAccountAddress, "set-agent-can-approve-revoke-contracts", @@ -460,7 +500,8 @@ describe("agent-workflow: contract approval flow", function () { describe("agent-workflow: voting via agent-account", function () { it("agent votes on proposal through agent-account", function () { - // arrange - construct DAO + // arrange - initialize and construct DAO + initializeAgentAccount(); constructDao(); // Approve voting contract for agent-account @@ -486,7 +527,7 @@ describe("agent-workflow: voting via agent-account", function () { // Advance to voting period mineBlocks(VOTING_DELAY + 1); - // Give agent-account some DAO tokens + // Give agent-account some sBTC mintMockSbtc(5000000, accountOwner); simnet.callPublicFn( agentAccountAddress, @@ -522,7 +563,8 @@ describe("agent-workflow: voting via agent-account", function () { }); it("agent cannot vote without approved voting contract", function () { - // arrange - construct DAO but do NOT approve voting contract + // arrange - initialize, construct DAO but do NOT approve voting contract + initializeAgentAccount(); constructDao(); // Give wallet1 tokens to create proposal @@ -557,7 +599,8 @@ describe("agent-workflow: voting via agent-account", function () { }); it("agent cannot vote after use-proposals permission revoked", function () { - // arrange - construct DAO and approve voting contract + // arrange - initialize, construct DAO and approve voting contract + initializeAgentAccount(); constructDao(); simnet.callPublicFn( agentAccountAddress, @@ -607,6 +650,7 @@ describe("agent-workflow: voting via agent-account", function () { describe("agent-workflow: STX handling", function () { it("owner deposits and withdraws STX", function () { // arrange + initializeAgentAccount(); const depositAmount = 1000000; // act - deposit STX @@ -634,6 +678,7 @@ describe("agent-workflow: STX handling", function () { it("agent deposits STX with permission", function () { // arrange + initializeAgentAccount(); const depositAmount = 500000; // act - agent deposits STX @@ -651,7 +696,8 @@ describe("agent-workflow: STX handling", function () { describe("agent-workflow: complete agent lifecycle", function () { it("full flow: deposit -> approve contract -> use contract -> withdraw", function () { - // arrange - construct DAO + // arrange - initialize and construct DAO + initializeAgentAccount(); constructDao(); // step 1: owner deposits tokens