;; ============================================
;; 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
)
)