Source Code

;; clarity-version 4
;; Simple Bitcoin Treasury DAO
;; Community manages pooled sBTC funds through voting

;; token definitions
(define-fungible-token sbtc-token)

;; constants
(define-constant err-not-member (err u101))
(define-constant err-proposal-not-found (err u102))
(define-constant err-voting-ended (err u103))
(define-constant err-insufficient-funds (err u104))
(define-constant err-already-voted (err u105))
(define-constant err-voting-not-ended (err u106))
(define-constant err-already-executed (err u107))
(define-constant err-proposal-rejected (err u108))
(define-constant err-asset-restriction-failed (err u109))
(define-constant err-invalid-signature (err u110))
(define-constant err-passkey-not-found (err u111))
(define-constant err-invalid-contract-hash (err u112))

;; Voting period: 24 hours (144 blocks * 10 minutes per block)
(define-constant voting-period-blocks u144)

;; data vars
(define-data-var proposal-nonce uint u0)
(define-data-var treasury-principal principal tx-sender)

;; data maps
(define-map dao-members principal bool)
(define-map proposals
  uint
  {
    creator: principal,
    amount: uint,
    recipient: principal,
    yes-votes: uint,
    no-votes: uint,
    end-block: uint,
    end-timestamp: uint,
    executed: bool,
    created-at: uint
  }
)
(define-map member-votes { proposal-id: uint, voter: principal } bool)

;; passkey storage for biometric authentication (Clarity 4)
(define-map member-passkeys principal (buff 33))

;; trusted contracts registry (Clarity 4)
(define-map trusted-contracts principal bool)

;; read-only functions (defined before use)
(define-read-only (is-member (user principal))
  (default-to false (map-get? dao-members user))
)

;; public functions
(define-public (join-dao)
  (begin
    (map-set dao-members tx-sender true)
    (ok true)
  )
)

;; Join DAO with passkey (secp256r1-verify - Clarity 4)
(define-public (join-dao-with-passkey (public-key (buff 33)) (message-hash (buff 32)) (signature (buff 64)))
  (begin
    (asserts! (secp256r1-verify message-hash signature public-key) err-invalid-signature)
    (map-set dao-members tx-sender true)
    (map-set member-passkeys tx-sender public-key)
    (ok true)
  )
)

;; Vote with passkey authentication (secp256r1-verify - Clarity 4)
(define-public (vote-with-passkey (proposal-id uint) (vote-yes bool) (message-hash (buff 32)) (signature (buff 64)))
  (let (
    (proposal (unwrap! (map-get? proposals proposal-id) err-proposal-not-found))
    (passkey (unwrap! (map-get? member-passkeys tx-sender) err-passkey-not-found))
  )
    (asserts! (secp256r1-verify message-hash signature passkey) err-invalid-signature)
    (asserts! (is-member tx-sender) err-not-member)
    (asserts! (< stacks-block-time (get end-timestamp proposal)) err-voting-ended)
    (asserts! (is-none (map-get? member-votes { proposal-id: proposal-id, voter: tx-sender })) err-already-voted)

    (map-set member-votes { proposal-id: proposal-id, voter: tx-sender } true)

    (if vote-yes
      (map-set proposals proposal-id (merge proposal { yes-votes: (+ (get yes-votes proposal) u1) }))
      (map-set proposals proposal-id (merge proposal { no-votes: (+ (get no-votes proposal) u1) }))
    )
    (ok true)
  )
)

;; Register trusted contract - simplified (Clarity 4)
(define-public (register-trusted-contract (contract principal))
  (begin
    (asserts! (is-member tx-sender) err-not-member)
    (map-set trusted-contracts contract true)
    (ok true)
  )
)

(define-public (deposit (amount uint))
  (begin
    (asserts! (is-member tx-sender) err-not-member)
    (ft-mint? sbtc-token amount tx-sender)
  )
)

(define-public (create-proposal (amount uint) (recipient principal))
  (let (
    (proposal-id (+ (var-get proposal-nonce) u1))
    (current-time stacks-block-time)
    (voting-deadline (+ stacks-block-time u86400)) ;; 24 hours in seconds
  )
    (asserts! (is-member tx-sender) err-not-member)
    (map-set proposals proposal-id {
      creator: tx-sender,
      amount: amount,
      recipient: recipient,
      yes-votes: u0,
      no-votes: u0,
      end-block: (+ stacks-block-height voting-period-blocks),
      end-timestamp: voting-deadline,
      executed: false,
      created-at: current-time
    })
    (var-set proposal-nonce proposal-id)
    (ok proposal-id)
  )
)

(define-public (vote (proposal-id uint) (vote-yes bool))
  (let ((proposal (unwrap! (map-get? proposals proposal-id) err-proposal-not-found)))
    (asserts! (is-member tx-sender) err-not-member)
    (asserts! (< stacks-block-time (get end-timestamp proposal)) err-voting-ended)
    (asserts! (is-none (map-get? member-votes { proposal-id: proposal-id, voter: tx-sender })) err-already-voted)

    (map-set member-votes { proposal-id: proposal-id, voter: tx-sender } true)

    (if vote-yes
      (map-set proposals proposal-id (merge proposal { yes-votes: (+ (get yes-votes proposal) u1) }))
      (map-set proposals proposal-id (merge proposal { no-votes: (+ (get no-votes proposal) u1) }))
    )
    (ok true)
  )
)

(define-public (execute-proposal (proposal-id uint))
  (let (
    (proposal (unwrap! (map-get? proposals proposal-id) err-proposal-not-found))
    (transfer-amount (get amount proposal))
    (transfer-recipient (get recipient proposal))
  )
    (asserts! (>= stacks-block-time (get end-timestamp proposal)) err-voting-not-ended)
    (asserts! (not (get executed proposal)) err-already-executed)
    (asserts! (> (get yes-votes proposal) (get no-votes proposal)) err-proposal-rejected)
    (asserts! (>= (ft-get-balance sbtc-token tx-sender) transfer-amount) err-insufficient-funds)

    ;; Execute transfer
    (try! (ft-transfer? sbtc-token transfer-amount tx-sender transfer-recipient))
    (map-set proposals proposal-id (merge proposal { executed: true }))
    (ok true)
  )
)

;; Batch vote on multiple proposals (Clarity 4 - uses stacks-block-time)
(define-public (batch-vote (proposal-ids (list 10 uint)) (votes (list 10 bool)))
  (begin
    (asserts! (is-member tx-sender) err-not-member)
    (asserts! (is-eq (len proposal-ids) (len votes)) (err u300))
    (ok (map batch-vote-helper proposal-ids votes))
  )
)

;; Helper for batch voting
(define-private (batch-vote-helper (proposal-id uint) (vote-yes bool))
  (match (map-get? proposals proposal-id)
    proposal
      (if (and
            (< stacks-block-time (get end-timestamp proposal))
            (is-none (map-get? member-votes { proposal-id: proposal-id, voter: tx-sender })))
        (begin
          (map-set member-votes { proposal-id: proposal-id, voter: tx-sender } true)
          (if vote-yes
            (map-set proposals proposal-id (merge proposal { yes-votes: (+ (get yes-votes proposal) u1) }))
            (map-set proposals proposal-id (merge proposal { no-votes: (+ (get no-votes proposal) u1) }))
          )
          true
        )
        false
      )
    false
  )
)

;; Update member passkey (secp256r1-verify - Clarity 4)
(define-public (update-passkey (new-public-key (buff 33)) (message-hash (buff 32)) (signature (buff 64)))
  (let ((old-passkey (unwrap! (map-get? member-passkeys tx-sender) err-passkey-not-found)))
    (asserts! (secp256r1-verify message-hash signature old-passkey) err-invalid-signature)
    (asserts! (is-member tx-sender) err-not-member)
    (map-set member-passkeys tx-sender new-public-key)
    (ok true)
  )
)

;; Cancel proposal before voting ends (Clarity 4 - uses stacks-block-time)
(define-public (cancel-proposal (proposal-id uint))
  (let ((proposal (unwrap! (map-get? proposals proposal-id) err-proposal-not-found)))
    (asserts! (is-eq tx-sender (get creator proposal)) (err u301))
    (asserts! (< stacks-block-time (get end-timestamp proposal)) err-voting-ended)
    (asserts! (not (get executed proposal)) err-already-executed)
    (map-set proposals proposal-id (merge proposal {
      executed: true,
      end-timestamp: stacks-block-time
    }))
    (ok true)
  )
)

;; Delegate voting power with passkey (secp256r1-verify - Clarity 4)
(define-map delegations { delegator: principal, proposal-id: uint } principal)

(define-public (delegate-vote (proposal-id uint) (delegate principal) (message-hash (buff 32)) (signature (buff 64)))
  (let (
    (proposal (unwrap! (map-get? proposals proposal-id) err-proposal-not-found))
    (passkey (unwrap! (map-get? member-passkeys tx-sender) err-passkey-not-found))
  )
    (asserts! (secp256r1-verify message-hash signature passkey) err-invalid-signature)
    (asserts! (is-member tx-sender) err-not-member)
    (asserts! (is-member delegate) err-not-member)
    (asserts! (< stacks-block-time (get end-timestamp proposal)) err-voting-ended)
    (asserts! (is-none (map-get? member-votes { proposal-id: proposal-id, voter: tx-sender })) err-already-voted)
    (map-set delegations { delegator: tx-sender, proposal-id: proposal-id } delegate)
    (ok true)
  )
)

;; Execute delegated vote
(define-public (execute-delegated-vote (proposal-id uint) (delegator principal) (vote-yes bool))
  (let (
    (proposal (unwrap! (map-get? proposals proposal-id) err-proposal-not-found))
    (delegate (unwrap! (map-get? delegations { delegator: delegator, proposal-id: proposal-id }) (err u302)))
  )
    (asserts! (is-eq tx-sender delegate) (err u303))
    (asserts! (< stacks-block-time (get end-timestamp proposal)) err-voting-ended)
    (asserts! (is-none (map-get? member-votes { proposal-id: proposal-id, voter: delegator })) err-already-voted)

    (map-set member-votes { proposal-id: proposal-id, voter: delegator } true)

    (if vote-yes
      (map-set proposals proposal-id (merge proposal { yes-votes: (+ (get yes-votes proposal) u1) }))
      (map-set proposals proposal-id (merge proposal { no-votes: (+ (get no-votes proposal) u1) }))
    )
    (ok true)
  )
)

;; Emergency withdraw for proposal creator if rejected (Clarity 4 - uses stacks-block-time)
(define-public (withdraw-rejected-proposal (proposal-id uint))
  (let ((proposal (unwrap! (map-get? proposals proposal-id) err-proposal-not-found)))
    (asserts! (is-eq tx-sender (get creator proposal)) (err u304))
    (asserts! (>= stacks-block-time (get end-timestamp proposal)) err-voting-not-ended)
    (asserts! (not (get executed proposal)) err-already-executed)
    (asserts! (<= (get yes-votes proposal) (get no-votes proposal)) (err u305))
    (map-set proposals proposal-id (merge proposal { executed: true }))
    (ok true)
  )
)

;; Extend voting period (Clarity 4 - uses stacks-block-time)
(define-public (extend-voting-period (proposal-id uint) (additional-seconds uint))
  (let ((proposal (unwrap! (map-get? proposals proposal-id) err-proposal-not-found)))
    (asserts! (is-eq tx-sender (get creator proposal)) (err u306))
    (asserts! (< stacks-block-time (get end-timestamp proposal)) err-voting-ended)
    (asserts! (not (get executed proposal)) err-already-executed)
    (asserts! (<= additional-seconds u172800) (err u307)) ;; max 48 hours extension
    (map-set proposals proposal-id (merge proposal {
      end-timestamp: (+ (get end-timestamp proposal) additional-seconds)
    }))
    (ok true)
  )
)

;; Revoke vote delegation (Clarity 4 - uses stacks-block-time)
(define-public (revoke-delegation (proposal-id uint))
  (let ((proposal (unwrap! (map-get? proposals proposal-id) err-proposal-not-found)))
    (asserts! (is-member tx-sender) err-not-member)
    (asserts! (< stacks-block-time (get end-timestamp proposal)) err-voting-ended)
    (asserts! (is-some (map-get? delegations { delegator: tx-sender, proposal-id: proposal-id })) (err u308))
    (map-delete delegations { delegator: tx-sender, proposal-id: proposal-id })
    (ok true)
  )
)

;; Bulk deposit for multiple members (Clarity 4)
(define-public (bulk-deposit (recipients (list 20 principal)) (amounts (list 20 uint)))
  (begin
    (asserts! (is-member tx-sender) err-not-member)
    (asserts! (is-eq (len recipients) (len amounts)) (err u309))
    (ok (map bulk-deposit-helper recipients amounts))
  )
)

;; Helper for bulk deposit
(define-private (bulk-deposit-helper (recipient principal) (amount uint))
  (match (ft-mint? sbtc-token amount recipient)
    success true
    error false
  )
)

;; additional read-only functions
(define-read-only (get-treasury-balance)
  (ft-get-balance sbtc-token tx-sender)
)

(define-read-only (get-proposal (proposal-id uint))
  (map-get? proposals proposal-id)
)

(define-read-only (get-proposal-status (proposal-id uint))
  (match (map-get? proposals proposal-id)
    proposal
      (if (get executed proposal)
        "executed"
        (if (>= stacks-block-time (get end-timestamp proposal))
          (if (> (get yes-votes proposal) (get no-votes proposal))
            "passed-pending-execution"
            "rejected"
          )
          "voting-active"
        )
      )
    "not-found"
  )
)

(define-read-only (get-voting-deadline-info (proposal-id uint))
  (match (map-get? proposals proposal-id)
    proposal
      (ok {
        end-timestamp: (get end-timestamp proposal),
        created-at: (get created-at proposal),
        time-remaining: (if (>= stacks-block-time (get end-timestamp proposal))
          u0
          (- (get end-timestamp proposal) stacks-block-time)
        ),
        is-active: (< stacks-block-time (get end-timestamp proposal))
      })
    err-proposal-not-found
  )
)

(define-read-only (has-voted (proposal-id uint) (voter principal))
  (is-some (map-get? member-votes { proposal-id: proposal-id, voter: voter }))
)

(define-read-only (get-proposal-results (proposal-id uint))
  (match (map-get? proposals proposal-id)
    proposal
      (ok {
        yes-votes: (get yes-votes proposal),
        no-votes: (get no-votes proposal),
        total-votes: (+ (get yes-votes proposal) (get no-votes proposal)),
        winning: (> (get yes-votes proposal) (get no-votes proposal))
      })
    err-proposal-not-found
  )
)

;; Get proposal ID as ASCII string (to-ascii? - Clarity 4)
(define-read-only (get-proposal-id-as-string (proposal-id uint))
  (to-ascii? proposal-id)
)

;; Get formatted proposal info with ASCII labels (to-ascii? - Clarity 4)
(define-read-only (get-proposal-summary (proposal-id uint))
  (match (map-get? proposals proposal-id)
    proposal
      (ok {
        id-string: (unwrap! (to-ascii? proposal-id) (err u200)),
        yes-string: (unwrap! (to-ascii? (get yes-votes proposal)) (err u201)),
        no-string: (unwrap! (to-ascii? (get no-votes proposal)) (err u202)),
        executed-string: (unwrap! (to-ascii? (get executed proposal)) (err u203))
      })
    err-proposal-not-found
  )
)

;; Verify if contract is trusted (Clarity 4)
(define-read-only (is-trusted-contract (contract principal))
  (default-to false (map-get? trusted-contracts contract))
)

;; Check if member has passkey registered
(define-read-only (has-passkey (member principal))
  (is-some (map-get? member-passkeys member))
)

;; Get delegation info (Clarity 4)
(define-read-only (get-delegation (delegator principal) (proposal-id uint))
  (map-get? delegations { delegator: delegator, proposal-id: proposal-id })
)

;; Check if proposal can be extended (Clarity 4 - uses stacks-block-time)
(define-read-only (can-extend-proposal (proposal-id uint))
  (match (map-get? proposals proposal-id)
    proposal
      (ok {
        can-extend: (and
          (< stacks-block-time (get end-timestamp proposal))
          (not (get executed proposal))
        ),
        time-until-deadline: (if (>= stacks-block-time (get end-timestamp proposal))
          u0
          (- (get end-timestamp proposal) stacks-block-time)
        )
      })
    err-proposal-not-found
  )
)

;; Get active proposals info (Clarity 4 - uses stacks-block-time)
(define-read-only (get-active-proposals-info)
  (ok {
    total-proposals: (var-get proposal-nonce),
    current-time: stacks-block-time
  })
)

Functions (33)

FunctionAccessArgs
is-memberread-onlyuser: principal
join-daopublic
join-dao-with-passkeypublicpublic-key: (buff 33
vote-with-passkeypublicproposal-id: uint, vote-yes: bool, message-hash: (buff 32
register-trusted-contractpubliccontract: principal
depositpublicamount: uint
create-proposalpublicamount: uint, recipient: principal
votepublicproposal-id: uint, vote-yes: bool
execute-proposalpublicproposal-id: uint
batch-votepublicproposal-ids: (list 10 uint
batch-vote-helperprivateproposal-id: uint, vote-yes: bool
update-passkeypublicnew-public-key: (buff 33
cancel-proposalpublicproposal-id: uint
delegate-votepublicproposal-id: uint, delegate: principal, message-hash: (buff 32
execute-delegated-votepublicproposal-id: uint, delegator: principal, vote-yes: bool
withdraw-rejected-proposalpublicproposal-id: uint
extend-voting-periodpublicproposal-id: uint, additional-seconds: uint
revoke-delegationpublicproposal-id: uint
bulk-depositpublicrecipients: (list 20 principal
bulk-deposit-helperprivaterecipient: principal, amount: uint
get-treasury-balanceread-only
get-proposalread-onlyproposal-id: uint
get-proposal-statusread-onlyproposal-id: uint
get-voting-deadline-inforead-onlyproposal-id: uint
has-votedread-onlyproposal-id: uint, voter: principal
get-proposal-resultsread-onlyproposal-id: uint
get-proposal-id-as-stringread-onlyproposal-id: uint
get-proposal-summaryread-onlyproposal-id: uint
is-trusted-contractread-onlycontract: principal
has-passkeyread-onlymember: principal
get-delegationread-onlydelegator: principal, proposal-id: uint
can-extend-proposalread-onlyproposal-id: uint
get-active-proposals-inforead-only