Source Code

;; Simple DAO core v6 (testing-only): locked-STX snapshot voting, treasury execution, and timelock.

(define-constant ERR_PROPOSAL_MISSING u100)
(define-constant ERR_VOTING_CLOSED u101)
(define-constant ERR_ALREADY_VOTED u102)
(define-constant ERR_ALREADY_EXECUTED u103)
(define-constant ERR_NOT_PASSED u104)
(define-constant ERR_TRANSFER_FAILED u105)
(define-constant ERR_INSUFFICIENT_POWER u106)
(define-constant ERR_ACTIVE_PROPOSAL u107)
(define-constant ERR_INVALID_CHOICE u108)
(define-constant ERR_TIMELOCK u109)
(define-constant ERR_INVALID_AMOUNT u110)

(define-constant CHOICE_AGAINST u0)
(define-constant CHOICE_FOR u1)

(define-constant VOTING_PERIOD u1440)
(define-constant MIN_QUORUM u1000000)
(define-constant PROPOSAL_THRESHOLD u1000000)
(define-constant EXECUTION_DELAY u144)
(define-constant EXECUTION_WINDOW u1440)

(define-data-var next-proposal-id uint u1)
(define-data-var last-proposal-end-height uint u0)

(define-map proposals
  { id: uint }
  {
    proposer: principal,
    recipient: principal,
    amount: uint,
    start-height: uint,
    end-height: uint,
    execute-after: uint,
    execute-before: uint,
    for-votes: uint,
    against-votes: uint,
    executed: bool,
  }
)

(define-map receipts
  {
    id: uint,
    voter: principal,
  }
  {
    choice: uint,
    weight: uint,
  }
)

(define-map locked
  { voter: principal }
  { balance: uint }
)

(define-private (get-locked (who principal))
  (default-to u0 (get balance (map-get? locked { voter: who })))
)

(define-private (proposal-passes (proposal {
  proposer: principal,
  recipient: principal,
  amount: uint,
  start-height: uint,
  end-height: uint,
  execute-after: uint,
  execute-before: uint,
  for-votes: uint,
  against-votes: uint,
  executed: bool,
}))
  (let (
      (for (get for-votes proposal))
      (against (get against-votes proposal))
    )
    (and (>= for MIN_QUORUM) (> for against))
  )
)

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

(define-read-only (get-locked-balance (who principal))
  (get-locked who)
)

(define-public (lock-stx (amount uint))
  (let ((current (get-locked tx-sender)))
    (if (<= amount u0)
      (err ERR_INVALID_AMOUNT)
      (if (<= stacks-block-height (var-get last-proposal-end-height))
        (err ERR_ACTIVE_PROPOSAL)
        (match (stx-transfer? amount tx-sender (as-contract tx-sender))
          ok-result (begin
            (map-set locked { voter: tx-sender } { balance: (+ current amount) })
            (ok ok-result)
          )
          err-code (err ERR_TRANSFER_FAILED)
        )
      )
    )
  )
)

(define-public (unlock-stx (amount uint))
  (let ((current (get-locked tx-sender)))
    (if (or (<= amount u0) (> amount current))
      (err ERR_INVALID_AMOUNT)
      (if (<= stacks-block-height (var-get last-proposal-end-height))
        (err ERR_ACTIVE_PROPOSAL)
        (match (as-contract (stx-transfer? amount tx-sender tx-sender))
          ok-result (begin
            (map-set locked { voter: tx-sender } { balance: (- current amount) })
            (ok ok-result)
          )
          err-code (err ERR_TRANSFER_FAILED)
        )
      )
    )
  )
)

(define-public (propose
    (recipient principal)
    (amount uint)
  )
  (let (
      (pid (var-get next-proposal-id))
      (power (get-locked tx-sender))
      (now stacks-block-height)
      (last-end (var-get last-proposal-end-height))
    )
    (if (<= now last-end)
      (err ERR_ACTIVE_PROPOSAL)
      (if (or (< power PROPOSAL_THRESHOLD) (<= amount u0))
        (err ERR_INSUFFICIENT_POWER)
        (let (
            (end-height (+ now VOTING_PERIOD))
            (execute-after (+ now VOTING_PERIOD EXECUTION_DELAY))
            (execute-before (+ now VOTING_PERIOD EXECUTION_DELAY EXECUTION_WINDOW))
          )
          (map-set proposals { id: pid } {
            proposer: tx-sender,
            recipient: recipient,
            amount: amount,
            start-height: now,
            end-height: end-height,
            execute-after: execute-after,
            execute-before: execute-before,
            for-votes: u0,
            against-votes: u0,
            executed: false,
          })
          (var-set next-proposal-id (+ pid u1))
          (var-set last-proposal-end-height end-height)
          (ok pid)
        )
      )
    )
  )
)

(define-public (cast-vote
    (proposal-id uint)
    (choice uint)
  )
  (let ((proposal (unwrap! (map-get? proposals { id: proposal-id }) (err ERR_PROPOSAL_MISSING))))
    (if (or
        (get executed proposal)
        (< stacks-block-height (get start-height proposal))
        (> stacks-block-height (get end-height proposal))
      )
      (err ERR_VOTING_CLOSED)
      (if (or (is-eq choice CHOICE_FOR) (is-eq choice CHOICE_AGAINST))
        (let (
            (weight (get-locked tx-sender))
            (for-delta (if (is-eq choice CHOICE_FOR) weight u0))
            (against-delta (if (is-eq choice CHOICE_AGAINST) weight u0))
          )
          (if (map-insert receipts {
              id: proposal-id,
              voter: tx-sender,
            } {
              choice: choice,
              weight: weight,
            })
            (begin
              (map-set proposals
                { id: proposal-id }
                (merge proposal {
                  for-votes: (+ (get for-votes proposal) for-delta),
                  against-votes: (+ (get against-votes proposal) against-delta),
                })
              )
              (ok true)
            )
            (err ERR_ALREADY_VOTED)
          )
        )
        (err ERR_INVALID_CHOICE)
      )
    )
  )
)

(define-public (execute (proposal-id uint))
  (let ((proposal (unwrap! (map-get? proposals { id: proposal-id }) (err ERR_PROPOSAL_MISSING))))
    (if (get executed proposal)
      (err ERR_ALREADY_EXECUTED)
      (if (< stacks-block-height (get execute-after proposal))
        (err ERR_TIMELOCK)
        (if (> stacks-block-height (get execute-before proposal))
          (err ERR_VOTING_CLOSED)
          (if (proposal-passes proposal)
            (match (as-contract (stx-transfer? (get amount proposal) tx-sender
              (get recipient proposal)
            ))
              ok-result (begin
                (map-set proposals
                  { id: proposal-id }
                  (merge proposal { executed: true })
                )
                (ok ok-result)
              )
              err-code (err ERR_TRANSFER_FAILED)
            )
            (err ERR_NOT_PASSED)
          )
        )
      )
    )
  )
)

Functions (7)

FunctionAccessArgs
get-lockedprivatewho: principal
proposal-passesprivateproposal: { proposer: principal, recipient: principal, amount: uint, start-height: uint, end-height: uint, execute-after: uint, execute-before: uint, for-votes: uint, against-votes: uint, executed: bool, }
get-proposalread-onlyproposal-id: uint
get-locked-balanceread-onlywho: principal
lock-stxpublicamount: uint
unlock-stxpublicamount: uint
executepublicproposal-id: uint