Source Code

;; ============================================
;; sSnapshot v2 - Bitcoin DAO Voting Layer
;; ============================================
;; A minimal, elegant governance protocol for Bitcoin DAOs on Stacks
;; Inspired by Snapshot.org - gasless off-chain voting with on-chain verification
;;
;; Core Principles:
;; - Voting is OFF-CHAIN (signed messages)
;; - Verification is ON-CHAIN (immutable, auditable)
;; - No automatic execution - advisory governance
;; - Simple, transparent fee model
;; - Beginner-friendly design
;; ============================================

;; ============================================
;; TRAITS
;; ============================================

;; Renamed trait alias to avoid conflict with map field
(use-trait voting-strategy-contract .voting-strategy-trait.voting-strategy-trait)

;; ============================================
;; CONSTANTS
;; ============================================

;; Error codes - clear and descriptive
(define-constant ERR_NOT_AUTHORIZED (err u100))
(define-constant ERR_DAO_NOT_FOUND (err u101))
(define-constant ERR_PROPOSAL_NOT_FOUND (err u102))
(define-constant ERR_INVALID_SNAPSHOT (err u103))
(define-constant ERR_INVALID_VOTING_WINDOW (err u104))
(define-constant ERR_PROPOSAL_NOT_ACTIVE (err u105))
(define-constant ERR_PROPOSAL_STILL_ACTIVE (err u106))
(define-constant ERR_ALREADY_COMMITTED (err u107))
(define-constant ERR_PAYMENT_FAILED (err u108))
(define-constant ERR_INVALID_NAME (err u109))
(define-constant ERR_INVALID_QUORUM (err u110))
(define-constant ERR_DAO_EXISTS (err u111))

;; Protocol configuration
(define-constant CONTRACT_OWNER tx-sender)
(define-constant MIN_VOTING_PERIOD u144)  ;; ~1 day in blocks
(define-constant MAX_VOTING_PERIOD u4320) ;; ~30 days in blocks

;; ============================================
;; DATA VARIABLES
;; ============================================

;; Fee configuration (in microSTX)
(define-data-var dao-registration-fee uint u1000000)  ;; 1 STX
(define-data-var proposal-creation-fee uint u1000000)  ;; 1 STX
(define-data-var result-commitment-fee uint u500000)   ;; 0.5 STX

;; Counters
(define-data-var dao-counter uint u0)
(define-data-var proposal-counter uint u0)

;; Protocol state
(define-data-var protocol-paused bool false)

;; ============================================
;; DATA MAPS
;; ============================================

;; DAO Registry
(define-map daos
  uint  ;; dao-id
  {
    name: (string-utf8 64),
    creator: principal,
    strategy: principal,
    quorum: uint,
    created-at: uint,
    proposal-count: uint,
    is-active: bool
  }
)

;; DAO name lookup (ensures unique names)
(define-map dao-names
  (string-utf8 64)
  uint
)

;; Proposal Registry
(define-map proposals
  uint  ;; proposal-id
  {
    dao-id: uint,
    title: (string-utf8 128),
    description-hash: (buff 32),
    creator: principal,
    snapshot-block: uint,
    start-block: uint,
    end-block: uint,
    created-at: uint,
    is-cancelled: bool
  }
)

;; Proposal Results (committed after voting ends)
(define-map proposal-results
  uint  ;; proposal-id
  {
    yes-votes: uint,
    no-votes: uint,
    abstain-votes: uint,
    total-voting-power: uint,
    merkle-root: (buff 32),
    committed-by: principal,
    committed-at: uint,
    quorum-reached: bool
  }
)

;; ============================================
;; EVENTS (via print)
;; ============================================

;; Event: DAO Registered
(define-private (emit-dao-registered (dao-id uint) (name (string-utf8 64)) (creator principal))
  (print {
    event: "DaoRegistered",
    dao-id: dao-id,
    name: name,
    creator: creator,
    block: block-height
  })
)

;; Event: Proposal Created
(define-private (emit-proposal-created (proposal-id uint) (dao-id uint) (title (string-utf8 128)) (creator principal))
  (print {
    event: "ProposalCreated",
    proposal-id: proposal-id,
    dao-id: dao-id,
    title: title,
    creator: creator,
    block: block-height
  })
)

;; Event: Results Committed
(define-private (emit-results-committed (proposal-id uint) (yes uint) (no uint) (abstain uint) (quorum-reached bool))
  (print {
    event: "ResultsCommitted",
    proposal-id: proposal-id,
    yes-votes: yes,
    no-votes: no,
    abstain-votes: abstain,
    quorum-reached: quorum-reached,
    block: block-height
  })
)

;; Event: Fee Updated
(define-private (emit-fee-updated (fee-type (string-ascii 32)) (old-fee uint) (new-fee uint))
  (print {
    event: "FeeUpdated",
    fee-type: fee-type,
    old-fee: old-fee,
    new-fee: new-fee,
    block: block-height
  })
)

;; ============================================
;; ADMIN FUNCTIONS
;; ============================================

;; Update DAO registration fee
(define-public (set-dao-registration-fee (new-fee uint))
  (let ((old-fee (var-get dao-registration-fee)))
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED)
    (var-set dao-registration-fee new-fee)
    (emit-fee-updated "dao-registration" old-fee new-fee)
    (ok true)
  )
)

;; Update proposal creation fee
(define-public (set-proposal-creation-fee (new-fee uint))
  (let ((old-fee (var-get proposal-creation-fee)))
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED)
    (var-set proposal-creation-fee new-fee)
    (emit-fee-updated "proposal-creation" old-fee new-fee)
    (ok true)
  )
)

;; Update result commitment fee
(define-public (set-result-commitment-fee (new-fee uint))
  (let ((old-fee (var-get result-commitment-fee)))
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED)
    (var-set result-commitment-fee new-fee)
    (emit-fee-updated "result-commitment" old-fee new-fee)
    (ok true)
  )
)

;; Pause/unpause protocol (emergency use)
(define-public (set-protocol-paused (paused bool))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED)
    (var-set protocol-paused paused)
    (ok true)
  )
)

;; ============================================
;; DAO REGISTRY FUNCTIONS
;; ============================================

;; Register a new DAO
(define-public (register-dao 
  (dao-name (string-utf8 64))
  (voting-strategy principal)
  (quorum uint)
)
  (let
    (
      (dao-id (+ (var-get dao-counter) u1))
      (fee (var-get dao-registration-fee))
    )
    ;; Validate inputs
    (asserts! (> (len dao-name) u0) ERR_INVALID_NAME)
    (asserts! (> quorum u0) ERR_INVALID_QUORUM)
    (asserts! (is-none (map-get? dao-names dao-name)) ERR_DAO_EXISTS)
    (asserts! (not (var-get protocol-paused)) ERR_NOT_AUTHORIZED)
    
    ;; Collect registration fee
    (try! (stx-transfer? fee tx-sender CONTRACT_OWNER))
    
    ;; Store DAO
    (map-set daos dao-id {
      name: dao-name,
      creator: tx-sender,
      strategy: voting-strategy,
      quorum: quorum,
      created-at: block-height,
      proposal-count: u0,
      is-active: true
    })
    
    ;; Register name
    (map-set dao-names dao-name dao-id)
    
    ;; Update counter
    (var-set dao-counter dao-id)
    
    ;; Emit event
    (emit-dao-registered dao-id dao-name tx-sender)
    
    (ok dao-id)
  )
)

;; Update DAO settings (creator only)
(define-public (update-dao-settings
  (dao-id uint)
  (new-quorum uint)
)
  (let
    (
      (dao (unwrap! (map-get? daos dao-id) ERR_DAO_NOT_FOUND))
    )
    ;; Only creator can update
    (asserts! (is-eq tx-sender (get creator dao)) ERR_NOT_AUTHORIZED)
    (asserts! (> new-quorum u0) ERR_INVALID_QUORUM)
    
    ;; Update DAO
    (map-set daos dao-id (merge dao { quorum: new-quorum }))
    
    (ok true)
  )
)

;; Deactivate DAO (creator only)
(define-public (deactivate-dao (dao-id uint))
  (let
    (
      (dao (unwrap! (map-get? daos dao-id) ERR_DAO_NOT_FOUND))
    )
    (asserts! (is-eq tx-sender (get creator dao)) ERR_NOT_AUTHORIZED)
    
    (map-set daos dao-id (merge dao { is-active: false }))
    
    (ok true)
  )
)

;; ============================================
;; PROPOSAL FUNCTIONS
;; ============================================

;; Create a new proposal
(define-public (create-proposal
  (dao-id uint)
  (title (string-utf8 128))
  (description-hash (buff 32))
  (snapshot-block uint)
  (start-block uint)
  (end-block uint)
)
  (let
    (
      (proposal-id (+ (var-get proposal-counter) u1))
      (dao (unwrap! (map-get? daos dao-id) ERR_DAO_NOT_FOUND))
      (fee (var-get proposal-creation-fee))
      (voting-period (- end-block start-block))
    )
    ;; Validate DAO is active
    (asserts! (get is-active dao) ERR_NOT_AUTHORIZED)
    (asserts! (not (var-get protocol-paused)) ERR_NOT_AUTHORIZED)
    
    ;; Validate title
    (asserts! (> (len title) u0) ERR_INVALID_NAME)
    
    ;; Validate snapshot block (must be <= start block and not in future)
    (asserts! (<= snapshot-block start-block) ERR_INVALID_SNAPSHOT)
    (asserts! (<= snapshot-block block-height) ERR_INVALID_SNAPSHOT)
    
    ;; Validate voting window
    (asserts! (< start-block end-block) ERR_INVALID_VOTING_WINDOW)
    (asserts! (>= voting-period MIN_VOTING_PERIOD) ERR_INVALID_VOTING_WINDOW)
    (asserts! (<= voting-period MAX_VOTING_PERIOD) ERR_INVALID_VOTING_WINDOW)
    
    ;; Collect proposal fee
    (try! (stx-transfer? fee tx-sender CONTRACT_OWNER))
    
    ;; Store proposal
    (map-set proposals proposal-id {
      dao-id: dao-id,
      title: title,
      description-hash: description-hash,
      creator: tx-sender,
      snapshot-block: snapshot-block,
      start-block: start-block,
      end-block: end-block,
      created-at: block-height,
      is-cancelled: false
    })
    
    ;; Update DAO proposal count
    (map-set daos dao-id (merge dao { 
      proposal-count: (+ (get proposal-count dao) u1) 
    }))
    
    ;; Update counter
    (var-set proposal-counter proposal-id)
    
    ;; Emit event
    (emit-proposal-created proposal-id dao-id title tx-sender)
    
    (ok proposal-id)
  )
)

;; Cancel proposal (creator only, before voting starts)
(define-public (cancel-proposal (proposal-id uint))
  (let
    (
      (proposal (unwrap! (map-get? proposals proposal-id) ERR_PROPOSAL_NOT_FOUND))
    )
    ;; Only creator can cancel
    (asserts! (is-eq tx-sender (get creator proposal)) ERR_NOT_AUTHORIZED)
    ;; Can only cancel before voting starts
    (asserts! (< block-height (get start-block proposal)) ERR_PROPOSAL_NOT_ACTIVE)
    
    (map-set proposals proposal-id (merge proposal { is-cancelled: true }))
    
    (ok true)
  )
)

;; ============================================
;; RESULT COMMITMENT
;; ============================================

;; Commit voting results after proposal ends
;; Anyone can commit results (trustless finalization)
(define-public (commit-results
  (proposal-id uint)
  (yes-votes uint)
  (no-votes uint)
  (abstain-votes uint)
  (merkle-root (buff 32))
)
  (let
    (
      (proposal (unwrap! (map-get? proposals proposal-id) ERR_PROPOSAL_NOT_FOUND))
      (dao (unwrap! (map-get? daos (get dao-id proposal)) ERR_DAO_NOT_FOUND))
      (fee (var-get result-commitment-fee))
      (total-votes (+ yes-votes (+ no-votes abstain-votes)))
      (quorum-reached (>= total-votes (get quorum dao)))
    )
    ;; Check proposal hasn't been cancelled
    (asserts! (not (get is-cancelled proposal)) ERR_NOT_AUTHORIZED)
    
    ;; Voting must have ended
    (asserts! (>= block-height (get end-block proposal)) ERR_PROPOSAL_STILL_ACTIVE)
    
    ;; Results not already committed
    (asserts! (is-none (map-get? proposal-results proposal-id)) ERR_ALREADY_COMMITTED)
    
    ;; Collect commitment fee
    (try! (stx-transfer? fee tx-sender CONTRACT_OWNER))
    
    ;; Store results
    (map-set proposal-results proposal-id {
      yes-votes: yes-votes,
      no-votes: no-votes,
      abstain-votes: abstain-votes,
      total-voting-power: total-votes,
      merkle-root: merkle-root,
      committed-by: tx-sender,
      committed-at: block-height,
      quorum-reached: quorum-reached
    })
    
    ;; Emit event
    (emit-results-committed proposal-id yes-votes no-votes abstain-votes quorum-reached)
    
    (ok true)
  )
)

;; ============================================
;; READ-ONLY FUNCTIONS
;; ============================================

;; Get DAO by ID
(define-read-only (get-dao (dao-id uint))
  (map-get? daos dao-id)
)

;; Get DAO by name
(define-read-only (get-dao-by-name (name (string-utf8 64)))
  (match (map-get? dao-names name)
    dao-id (map-get? daos dao-id)
    none
  )
)

;; Get proposal by ID
(define-read-only (get-proposal (proposal-id uint))
  (map-get? proposals proposal-id)
)

;; Get proposal results
(define-read-only (get-results (proposal-id uint))
  (map-get? proposal-results proposal-id)
)

;; Check if proposal is currently active (voting open)
(define-read-only (is-proposal-active (proposal-id uint))
  (match (map-get? proposals proposal-id)
    proposal (and
      (not (get is-cancelled proposal))
      (>= block-height (get start-block proposal))
      (< block-height (get end-block proposal))
    )
    false
  )
)

;; Check if proposal voting has ended
(define-read-only (is-proposal-ended (proposal-id uint))
  (match (map-get? proposals proposal-id)
    proposal (>= block-height (get end-block proposal))
    false
  )
)

;; Check if results have been committed
(define-read-only (are-results-committed (proposal-id uint))
  (is-some (map-get? proposal-results proposal-id))
)

;; Get proposal status (human-readable)
(define-read-only (get-proposal-status (proposal-id uint))
  (match (map-get? proposals proposal-id)
    proposal 
      (if (get is-cancelled proposal)
        "cancelled"
        (if (< block-height (get start-block proposal))
          "pending"
          (if (< block-height (get end-block proposal))
            "active"
            (if (is-some (map-get? proposal-results proposal-id))
              "finalized"
              "ended"
            )
          )
        )
      )
    "not-found"
  )
)

;; Get current fees
(define-read-only (get-fees)
  {
    dao-registration: (var-get dao-registration-fee),
    proposal-creation: (var-get proposal-creation-fee),
    result-commitment: (var-get result-commitment-fee)
  }
)

;; Get protocol stats
(define-read-only (get-protocol-stats)
  {
    total-daos: (var-get dao-counter),
    total-proposals: (var-get proposal-counter),
    is-paused: (var-get protocol-paused),
    contract-owner: CONTRACT_OWNER
  }
)

;; Get DAO count
(define-read-only (get-dao-count)
  (var-get dao-counter)
)

;; Get proposal count
(define-read-only (get-proposal-count)
  (var-get proposal-counter)
)

;; Verify a vote inclusion (placeholder for Merkle proof verification)
;; In production, this would validate the Merkle proof
(define-read-only (verify-vote-inclusion
  (proposal-id uint)
  (voter principal)
  (vote-type uint)
  (voting-power uint)
  (proof (list 10 (buff 32)))
)
  (match (map-get? proposal-results proposal-id)
    results true  ;; Simplified - would verify Merkle proof
    false
  )
)

Functions (26)

FunctionAccessArgs
emit-dao-registeredprivatedao-id: uint, name: (string-utf8 64
emit-proposal-createdprivateproposal-id: uint, dao-id: uint, title: (string-utf8 128
emit-results-committedprivateproposal-id: uint, yes: uint, no: uint, abstain: uint, quorum-reached: bool
emit-fee-updatedprivatefee-type: (string-ascii 32
set-dao-registration-feepublicnew-fee: uint
set-proposal-creation-feepublicnew-fee: uint
set-result-commitment-feepublicnew-fee: uint
set-protocol-pausedpublicpaused: bool
register-daopublicdao-name: (string-utf8 64
deactivate-daopublicdao-id: uint
create-proposalpublicdao-id: uint, title: (string-utf8 128
cancel-proposalpublicproposal-id: uint
commit-resultspublicproposal-id: uint, yes-votes: uint, no-votes: uint, abstain-votes: uint, merkle-root: (buff 32
get-daoread-onlydao-id: uint
get-dao-by-nameread-onlyname: (string-utf8 64
get-proposalread-onlyproposal-id: uint
get-resultsread-onlyproposal-id: uint
is-proposal-activeread-onlyproposal-id: uint
is-proposal-endedread-onlyproposal-id: uint
are-results-committedread-onlyproposal-id: uint
get-proposal-statusread-onlyproposal-id: uint
get-feesread-only
get-protocol-statsread-only
get-dao-countread-only
get-proposal-countread-only
verify-vote-inclusionread-onlyproposal-id: uint, voter: principal, vote-type: uint, voting-power: uint, proof: (list 10 (buff 32