Source Code

;; ============================================================
;; StacksDAO Governance Contract
;; ============================================================
;; On-chain DAO governance powered by SDAO tokens.
;; Token holders can create proposals, vote, and execute
;; approved proposals after the voting window closes.
;; ============================================================

;; ---------------------
;; Constants
;; ---------------------
(define-constant CONTRACT-OWNER tx-sender)
(define-constant VOTING-PERIOD u144)                ;; ~1 day (144 blocks x 10 min)
(define-constant MIN-PROPOSAL-BALANCE u100000000)   ;; 100 SDAO (with 6 decimals) to create proposal
(define-constant QUORUM u500000000)                 ;; 500 SDAO minimum total votes for quorum
(define-constant ERR-NOT-AUTHORIZED (err u401))
(define-constant ERR-PROPOSAL-NOT-FOUND (err u701))
(define-constant ERR-ALREADY-VOTED (err u702))
(define-constant ERR-VOTING-ENDED (err u703))
(define-constant ERR-VOTING-ACTIVE (err u704))
(define-constant ERR-QUORUM-NOT-MET (err u705))
(define-constant ERR-PROPOSAL-REJECTED (err u706))
(define-constant ERR-ALREADY-EXECUTED (err u707))
(define-constant ERR-INSUFFICIENT-BALANCE (err u708))
(define-constant ERR-VOTING-NOT-ENDED (err u709))

;; ---------------------
;; Data Variables
;; ---------------------
(define-data-var proposal-count uint u0)

;; ---------------------
;; Data Maps
;; ---------------------

;; Proposal storage
(define-map proposals uint
  {
    title: (string-utf8 256),
    description: (string-utf8 1024),
    proposer: principal,
    start-block: uint,
    end-block: uint,
    votes-for: uint,
    votes-against: uint,
    executed: bool,
    total-votes: uint
  }
)

;; Track votes per user per proposal
(define-map votes { proposal-id: uint, voter: principal }
  {
    amount: uint,
    in-favor: bool
  }
)

;; ---------------------
;; Create Proposal
;; ---------------------
(define-public (create-proposal
    (title (string-utf8 256))
    (description (string-utf8 1024))
  )
  (let
    (
      (proposer-balance (unwrap-panic (contract-call? .governance-token-v2 get-balance tx-sender)))
      (new-id (+ (var-get proposal-count) u1))
    )
    ;; Must hold minimum SDAO to propose
    (asserts! (>= proposer-balance MIN-PROPOSAL-BALANCE) ERR-INSUFFICIENT-BALANCE)

    ;; Create the proposal
    (map-set proposals new-id {
      title: title,
      description: description,
      proposer: tx-sender,
      start-block: block-height,
      end-block: (+ block-height VOTING-PERIOD),
      votes-for: u0,
      votes-against: u0,
      executed: false,
      total-votes: u0
    })

    (var-set proposal-count new-id)

    (print { event: "proposal-created", id: new-id, proposer: tx-sender, title: title })
    (ok new-id)
  )
)

;; ---------------------
;; Vote For
;; ---------------------
(define-public (vote-for (proposal-id uint))
  (let
    (
      (proposal (unwrap! (map-get? proposals proposal-id) ERR-PROPOSAL-NOT-FOUND))
      (voter-balance (unwrap-panic (contract-call? .governance-token-v2 get-balance tx-sender)))
    )
    ;; Voting must be active
    (asserts! (<= block-height (get end-block proposal)) ERR-VOTING-ENDED)
    ;; Cannot vote twice
    (asserts! (is-none (map-get? votes { proposal-id: proposal-id, voter: tx-sender })) ERR-ALREADY-VOTED)
    ;; Must hold tokens
    (asserts! (> voter-balance u0) ERR-INSUFFICIENT-BALANCE)

    ;; Record vote
    (map-set votes { proposal-id: proposal-id, voter: tx-sender }
      { amount: voter-balance, in-favor: true }
    )

    ;; Update proposal tallies
    (map-set proposals proposal-id
      (merge proposal {
        votes-for: (+ (get votes-for proposal) voter-balance),
        total-votes: (+ (get total-votes proposal) voter-balance)
      })
    )

    (print { event: "vote-cast", proposal-id: proposal-id, voter: tx-sender, in-favor: true, weight: voter-balance })
    (ok true)
  )
)

;; ---------------------
;; Vote Against
;; ---------------------
(define-public (vote-against (proposal-id uint))
  (let
    (
      (proposal (unwrap! (map-get? proposals proposal-id) ERR-PROPOSAL-NOT-FOUND))
      (voter-balance (unwrap-panic (contract-call? .governance-token-v2 get-balance tx-sender)))
    )
    ;; Voting must be active
    (asserts! (<= block-height (get end-block proposal)) ERR-VOTING-ENDED)
    ;; Cannot vote twice
    (asserts! (is-none (map-get? votes { proposal-id: proposal-id, voter: tx-sender })) ERR-ALREADY-VOTED)
    ;; Must hold tokens
    (asserts! (> voter-balance u0) ERR-INSUFFICIENT-BALANCE)

    ;; Record vote
    (map-set votes { proposal-id: proposal-id, voter: tx-sender }
      { amount: voter-balance, in-favor: false }
    )

    ;; Update proposal tallies
    (map-set proposals proposal-id
      (merge proposal {
        votes-against: (+ (get votes-against proposal) voter-balance),
        total-votes: (+ (get total-votes proposal) voter-balance)
      })
    )

    (print { event: "vote-cast", proposal-id: proposal-id, voter: tx-sender, in-favor: false, weight: voter-balance })
    (ok true)
  )
)

;; ---------------------
;; Execute Proposal
;; ---------------------
(define-public (execute-proposal (proposal-id uint))
  (let
    (
      (proposal (unwrap! (map-get? proposals proposal-id) ERR-PROPOSAL-NOT-FOUND))
    )
    ;; Voting period must be over
    (asserts! (> block-height (get end-block proposal)) ERR-VOTING-NOT-ENDED)
    ;; Cannot execute twice
    (asserts! (not (get executed proposal)) ERR-ALREADY-EXECUTED)
    ;; Quorum must be met
    (asserts! (>= (get total-votes proposal) QUORUM) ERR-QUORUM-NOT-MET)
    ;; Must have more votes for than against
    (asserts! (> (get votes-for proposal) (get votes-against proposal)) ERR-PROPOSAL-REJECTED)

    ;; Mark as executed
    (map-set proposals proposal-id
      (merge proposal { executed: true })
    )

    (print { event: "proposal-executed", proposal-id: proposal-id })
    (ok true)
  )
)

;; ---------------------
;; Read-Only Functions
;; ---------------------

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

(define-read-only (get-proposal-count)
  (ok (var-get proposal-count))
)

(define-read-only (get-vote (proposal-id uint) (voter principal))
  (map-get? votes { proposal-id: proposal-id, voter: voter })
)

(define-read-only (is-voting-active (proposal-id uint))
  (match (map-get? proposals proposal-id)
    proposal (ok (<= block-height (get end-block proposal)))
    ERR-PROPOSAL-NOT-FOUND
  )
)

(define-read-only (get-voting-period)
  (ok VOTING-PERIOD)
)

(define-read-only (get-min-proposal-balance)
  (ok MIN-PROPOSAL-BALANCE)
)

(define-read-only (get-quorum)
  (ok QUORUM)
)

Functions (11)

FunctionAccessArgs
create-proposalpublictitle: (string-utf8 256
vote-forpublicproposal-id: uint
vote-againstpublicproposal-id: uint
execute-proposalpublicproposal-id: uint
get-proposalread-onlyproposal-id: uint
get-proposal-countread-only
get-voteread-onlyproposal-id: uint, voter: principal
is-voting-activeread-onlyproposal-id: uint
get-voting-periodread-only
get-min-proposal-balanceread-only
get-quorumread-only