Source Code

;; Governance Contract V2
;; Upgraded proposal creation, voting, delegation, and execution for DAO
;; Clarity 4
;;
;; UPGRADES FROM V1:
;; - Token-integrated voting power (reads directly from dao-token)
;; - Delegation system for voting power
;; - Timelock mechanism for proposal execution
;; - Dynamic quorum based on token supply
;; - Proposal cancellation by proposer
;; - Vote change capability before voting ends

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

(define-constant CONTRACT-OWNER tx-sender)
(define-constant ERR-NOT-AUTHORIZED (err u200))
(define-constant ERR-PROPOSAL-NOT-FOUND (err u201))
(define-constant ERR-PROPOSAL-EXPIRED (err u202))
(define-constant ERR-PROPOSAL-NOT-ACTIVE (err u203))
(define-constant ERR-ALREADY-VOTED (err u204))
(define-constant ERR-INSUFFICIENT-TOKENS (err u205))
(define-constant ERR-PROPOSAL-NOT-PASSED (err u206))
(define-constant ERR-PROPOSAL-ALREADY-EXECUTED (err u207))
(define-constant ERR-VOTING-NOT-ENDED (err u208))
(define-constant ERR-INVALID-PROPOSAL (err u209))
(define-constant ERR-QUORUM-NOT-MET (err u210))
(define-constant ERR-TIMELOCK-NOT-EXPIRED (err u211))
(define-constant ERR-PROPOSAL-CANCELLED (err u212))
(define-constant ERR-CANNOT-DELEGATE-TO-SELF (err u213))
(define-constant ERR-CIRCULAR-DELEGATION (err u214))
(define-constant ERR-NO-VOTE-TO-CHANGE (err u215))
(define-constant ERR-DELEGATION-EXISTS (err u216))

;; Voting parameters
(define-constant VOTING-PERIOD u144)              ;; ~1 day in blocks (assuming 10 min blocks)
(define-constant TIMELOCK-PERIOD u72)             ;; ~12 hours delay before execution
(define-constant MIN-PROPOSAL-TOKENS u1000000000) ;; Minimum 1000 tokens to create proposal
(define-constant QUORUM-PERCENTAGE u10)           ;; 10% of total supply required for quorum
(define-constant APPROVAL-THRESHOLD u51)          ;; 51% approval required

;; Proposal status
(define-constant STATUS-ACTIVE u1)
(define-constant STATUS-PASSED u2)
(define-constant STATUS-REJECTED u3)
(define-constant STATUS-EXECUTED u4)
(define-constant STATUS-EXPIRED u5)
(define-constant STATUS-CANCELLED u6)

;; Proposal types
(define-constant TYPE-GENERAL u1)
(define-constant TYPE-FUNDING u2)
(define-constant TYPE-PARAMETER u3)
(define-constant TYPE-EMERGENCY u4)

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

(define-data-var proposal-count uint u0)
(define-data-var token-contract principal .dao-token)

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

;; Proposal storage (enhanced)
(define-map proposals
  uint
  {
    proposer: principal,
    title: (string-ascii 100),
    description: (string-utf8 500),
    proposal-type: uint,
    start-block: uint,
    end-block: uint,
    execution-block: uint,
    votes-for: uint,
    votes-against: uint,
    status: uint,
    execution-data: (optional (buff 256)),
    total-supply-snapshot: uint
  }
)

;; Track who has voted on which proposal
(define-map votes
  { proposal-id: uint, voter: principal }
  { amount: uint, vote-for: bool }
)

;; Voting power snapshot at proposal creation
(define-map voting-power-snapshot
  { proposal-id: uint, voter: principal }
  uint
)

;; Delegation system
(define-map delegations
  principal  ;; delegator
  principal  ;; delegate (who receives the voting power)
)

;; Track delegated power received
(define-map delegated-power-received
  principal
  uint
)

;; Track if user has active delegation
(define-map has-delegated principal bool)

;; =====================
;; PRIVATE FUNCTIONS
;; =====================

;; Get token balance from dao-token contract
(define-private (get-token-balance (account principal))
  (unwrap-panic (contract-call? .dao-token get-balance account))
)

;; Get total token supply from dao-token contract
(define-private (get-total-token-supply)
  (unwrap-panic (contract-call? .dao-token get-total-supply))
)

;; Calculate effective voting power (own tokens + delegated power)
(define-private (calculate-voting-power (voter principal))
  (let (
    (own-balance (get-token-balance voter))
    (delegated-to-voter (default-to u0 (map-get? delegated-power-received voter)))
    (has-delegated-away (default-to false (map-get? has-delegated voter)))
  )
    ;; If user has delegated their power away, they can't vote with own tokens
    (if has-delegated-away
      delegated-to-voter  ;; Only use power delegated TO them
      (+ own-balance delegated-to-voter)  ;; Own tokens + delegated power
    )
  )
)

;; Calculate dynamic quorum based on total supply
(define-private (calculate-quorum (total-supply uint))
  (/ (* total-supply QUORUM-PERCENTAGE) u100)
)

;; =====================
;; DELEGATION FUNCTIONS
;; =====================

;; Delegate voting power to another address
(define-public (delegate (delegate-to principal))
  (let (
    (delegator tx-sender)
    (delegator-balance (get-token-balance delegator))
  )
    ;; Cannot delegate to self
    (asserts! (not (is-eq delegator delegate-to)) ERR-CANNOT-DELEGATE-TO-SELF)
    ;; Must have tokens to delegate
    (asserts! (> delegator-balance u0) ERR-INSUFFICIENT-TOKENS)
    ;; Check for circular delegation (delegate-to shouldn't have delegated to delegator)
    (asserts! (not (is-eq (default-to delegate-to (map-get? delegations delegate-to)) delegator)) 
              ERR-CIRCULAR-DELEGATION)
    ;; Cannot delegate if already delegated (must revoke first)
    (asserts! (not (default-to false (map-get? has-delegated delegator))) ERR-DELEGATION-EXISTS)
    
    ;; Set delegation
    (map-set delegations delegator delegate-to)
    (map-set has-delegated delegator true)
    
    ;; Update delegated power received by delegate
    (map-set delegated-power-received delegate-to
      (+ (default-to u0 (map-get? delegated-power-received delegate-to)) delegator-balance))
    
    (print { 
      event: "delegation-created", 
      delegator: delegator, 
      delegate: delegate-to, 
      amount: delegator-balance 
    })
    (ok true)
  )
)

;; Revoke delegation
(define-public (revoke-delegation)
  (let (
    (delegator tx-sender)
    (delegate-to (unwrap! (map-get? delegations delegator) ERR-NOT-AUTHORIZED))
    (delegator-balance (get-token-balance delegator))
  )
    ;; Remove delegation
    (map-delete delegations delegator)
    (map-set has-delegated delegator false)
    
    ;; Update delegated power (subtract)
    (let ((current-delegated (default-to u0 (map-get? delegated-power-received delegate-to))))
      (map-set delegated-power-received delegate-to
        (if (>= current-delegated delegator-balance)
          (- current-delegated delegator-balance)
          u0
        ))
    )
    
    (print { event: "delegation-revoked", delegator: delegator, delegate: delegate-to })
    (ok true)
  )
)

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

;; Create a new proposal
(define-public (create-proposal 
  (title (string-ascii 100)) 
  (description (string-utf8 500))
  (proposal-type uint)
  (execution-data (optional (buff 256))))
  (let (
    (proposer tx-sender)
    (proposal-id (+ (var-get proposal-count) u1))
    (current-block stacks-block-height)
    (proposer-power (calculate-voting-power proposer))
    (total-supply (get-total-token-supply))
  )
    ;; Check minimum token requirement
    (asserts! (>= proposer-power MIN-PROPOSAL-TOKENS) ERR-INSUFFICIENT-TOKENS)
    ;; Validate proposal type
    (asserts! (and (>= proposal-type TYPE-GENERAL) (<= proposal-type TYPE-EMERGENCY)) ERR-INVALID-PROPOSAL)
    
    ;; Create proposal with total supply snapshot for quorum calculation
    (map-set proposals proposal-id {
      proposer: proposer,
      title: title,
      description: description,
      proposal-type: proposal-type,
      start-block: current-block,
      end-block: (+ current-block VOTING-PERIOD),
      execution-block: (+ current-block VOTING-PERIOD TIMELOCK-PERIOD),
      votes-for: u0,
      votes-against: u0,
      status: STATUS-ACTIVE,
      execution-data: execution-data,
      total-supply-snapshot: total-supply
    })
    
    ;; Update proposal count
    (var-set proposal-count proposal-id)
    
    (print { 
      event: "proposal-created", 
      proposal-id: proposal-id, 
      proposer: proposer, 
      title: title,
      proposal-type: proposal-type,
      end-block: (+ current-block VOTING-PERIOD),
      execution-block: (+ current-block VOTING-PERIOD TIMELOCK-PERIOD),
      total-supply-snapshot: total-supply
    })
    (ok proposal-id)
  )
)

;; Cancel a proposal (only by proposer, only if still active)
(define-public (cancel-proposal (proposal-id uint))
  (let (
    (proposal (unwrap! (map-get? proposals proposal-id) ERR-PROPOSAL-NOT-FOUND))
  )
    ;; Only proposer can cancel
    (asserts! (is-eq tx-sender (get proposer proposal)) ERR-NOT-AUTHORIZED)
    ;; Can only cancel active proposals
    (asserts! (is-eq (get status proposal) STATUS-ACTIVE) ERR-PROPOSAL-NOT-ACTIVE)
    
    ;; Mark as cancelled
    (map-set proposals proposal-id (merge proposal { status: STATUS-CANCELLED }))
    
    (print { event: "proposal-cancelled", proposal-id: proposal-id, proposer: tx-sender })
    (ok true)
  )
)

;; Cast a vote on a proposal
(define-public (vote (proposal-id uint) (vote-for bool))
  (let (
    (voter tx-sender)
    (proposal (unwrap! (map-get? proposals proposal-id) ERR-PROPOSAL-NOT-FOUND))
    (voter-power (calculate-voting-power voter))
  )
    ;; Check proposal is active
    (asserts! (is-eq (get status proposal) STATUS-ACTIVE) ERR-PROPOSAL-NOT-ACTIVE)
    ;; Check voting period
    (asserts! (<= stacks-block-height (get end-block proposal)) ERR-PROPOSAL-EXPIRED)
    ;; Check hasn't voted already
    (asserts! (is-none (map-get? votes { proposal-id: proposal-id, voter: voter })) ERR-ALREADY-VOTED)
    ;; Check has voting power
    (asserts! (> voter-power u0) ERR-INSUFFICIENT-TOKENS)
    
    ;; Record vote
    (map-set votes 
      { proposal-id: proposal-id, voter: voter }
      { amount: voter-power, vote-for: vote-for }
    )
    
    ;; Snapshot voting power
    (map-set voting-power-snapshot
      { proposal-id: proposal-id, voter: voter }
      voter-power
    )
    
    ;; Update vote counts
    (if vote-for
      (map-set proposals proposal-id (merge proposal { votes-for: (+ (get votes-for proposal) voter-power) }))
      (map-set proposals proposal-id (merge proposal { votes-against: (+ (get votes-against proposal) voter-power) }))
    )
    
    (print { 
      event: "vote-cast", 
      proposal-id: proposal-id, 
      voter: voter, 
      vote-for: vote-for, 
      amount: voter-power 
    })
    (ok true)
  )
)

;; Change vote on a proposal (before voting ends)
(define-public (change-vote (proposal-id uint) (new-vote-for bool))
  (let (
    (voter tx-sender)
    (proposal (unwrap! (map-get? proposals proposal-id) ERR-PROPOSAL-NOT-FOUND))
    (existing-vote (unwrap! (map-get? votes { proposal-id: proposal-id, voter: voter }) ERR-NO-VOTE-TO-CHANGE))
    (vote-amount (get amount existing-vote))
    (old-vote-for (get vote-for existing-vote))
  )
    ;; Check proposal is active
    (asserts! (is-eq (get status proposal) STATUS-ACTIVE) ERR-PROPOSAL-NOT-ACTIVE)
    ;; Check voting period
    (asserts! (<= stacks-block-height (get end-block proposal)) ERR-PROPOSAL-EXPIRED)
    
    ;; Only process if vote is actually changing
    (if (is-eq old-vote-for new-vote-for)
      (ok true)  ;; No change needed
      (begin
        ;; Update vote record
        (map-set votes 
          { proposal-id: proposal-id, voter: voter }
          { amount: vote-amount, vote-for: new-vote-for }
        )
        
        ;; Update vote counts (remove from old, add to new)
        (if old-vote-for
          ;; Was FOR, now AGAINST
          (map-set proposals proposal-id (merge proposal { 
            votes-for: (- (get votes-for proposal) vote-amount),
            votes-against: (+ (get votes-against proposal) vote-amount)
          }))
          ;; Was AGAINST, now FOR
          (map-set proposals proposal-id (merge proposal { 
            votes-for: (+ (get votes-for proposal) vote-amount),
            votes-against: (- (get votes-against proposal) vote-amount)
          }))
        )
        
        (print { 
          event: "vote-changed", 
          proposal-id: proposal-id, 
          voter: voter, 
          new-vote-for: new-vote-for,
          amount: vote-amount 
        })
        (ok true)
      )
    )
  )
)

;; Finalize a proposal after voting ends
(define-public (finalize-proposal (proposal-id uint))
  (let (
    (proposal (unwrap! (map-get? proposals proposal-id) ERR-PROPOSAL-NOT-FOUND))
    (total-votes (+ (get votes-for proposal) (get votes-against proposal)))
    (quorum-threshold (calculate-quorum (get total-supply-snapshot proposal)))
  )
    ;; Check proposal is still active
    (asserts! (is-eq (get status proposal) STATUS-ACTIVE) ERR-PROPOSAL-NOT-ACTIVE)
    ;; Check voting period has ended
    (asserts! (> stacks-block-height (get end-block proposal)) ERR-VOTING-NOT-ENDED)
    
    ;; Determine outcome based on dynamic quorum
    (if (< total-votes quorum-threshold)
      ;; Quorum not met - expired
      (begin
        (map-set proposals proposal-id (merge proposal { status: STATUS-EXPIRED }))
        (print { 
          event: "proposal-expired", 
          proposal-id: proposal-id, 
          reason: "quorum-not-met",
          total-votes: total-votes,
          quorum-required: quorum-threshold
        })
        (ok STATUS-EXPIRED)
      )
      ;; Check if passed (51% threshold)
      (if (> (* (get votes-for proposal) u100) (* total-votes APPROVAL-THRESHOLD))
        (begin
          (map-set proposals proposal-id (merge proposal { status: STATUS-PASSED }))
          (print { 
            event: "proposal-passed", 
            proposal-id: proposal-id,
            votes-for: (get votes-for proposal),
            votes-against: (get votes-against proposal),
            execution-block: (get execution-block proposal)
          })
          (ok STATUS-PASSED)
        )
        (begin
          (map-set proposals proposal-id (merge proposal { status: STATUS-REJECTED }))
          (print { 
            event: "proposal-rejected", 
            proposal-id: proposal-id,
            votes-for: (get votes-for proposal),
            votes-against: (get votes-against proposal)
          })
          (ok STATUS-REJECTED)
        )
      )
    )
  )
)

;; Execute a passed proposal (after timelock)
(define-public (execute-proposal (proposal-id uint))
  (let (
    (proposal (unwrap! (map-get? proposals proposal-id) ERR-PROPOSAL-NOT-FOUND))
  )
    ;; Check proposal passed
    (asserts! (is-eq (get status proposal) STATUS-PASSED) ERR-PROPOSAL-NOT-PASSED)
    ;; Check timelock has expired
    (asserts! (>= stacks-block-height (get execution-block proposal)) ERR-TIMELOCK-NOT-EXPIRED)
    
    ;; Mark as executed
    (map-set proposals proposal-id (merge proposal { status: STATUS-EXECUTED }))
    
    (print { 
      event: "proposal-executed", 
      proposal-id: proposal-id,
      execution-data: (get execution-data proposal),
      executed-at-block: stacks-block-height
    })
    (ok true)
  )
)

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

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

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

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

;; Check if proposal is active
(define-read-only (is-proposal-active (proposal-id uint))
  (match (map-get? proposals proposal-id)
    proposal (and 
      (is-eq (get status proposal) STATUS-ACTIVE)
      (<= stacks-block-height (get end-block proposal))
    )
    false
  )
)

;; Check if proposal is in timelock
(define-read-only (is-in-timelock (proposal-id uint))
  (match (map-get? proposals proposal-id)
    proposal (and 
      (is-eq (get status proposal) STATUS-PASSED)
      (< stacks-block-height (get execution-block proposal))
    )
    false
  )
)

;; Get blocks remaining in timelock
(define-read-only (get-timelock-remaining (proposal-id uint))
  (match (map-get? proposals proposal-id)
    proposal 
      (if (>= stacks-block-height (get execution-block proposal))
        u0
        (- (get execution-block proposal) stacks-block-height)
      )
    u0
  )
)

;; Get voting power for a member (real-time from token contract)
(define-read-only (get-voting-power (member principal))
  (calculate-voting-power member)
)

;; Get delegation info
(define-read-only (get-delegation (delegator principal))
  (map-get? delegations delegator)
)

;; Get delegated power received by address
(define-read-only (get-delegated-power (delegate-address principal))
  (default-to u0 (map-get? delegated-power-received delegate-address))
)

;; Check if address has delegated their power
(define-read-only (has-delegated-power (delegator principal))
  (default-to false (map-get? has-delegated delegator))
)

;; Calculate if proposal would pass with current votes
(define-read-only (would-proposal-pass (proposal-id uint))
  (match (map-get? proposals proposal-id)
    proposal 
      (let (
        (total-votes (+ (get votes-for proposal) (get votes-against proposal)))
        (quorum (calculate-quorum (get total-supply-snapshot proposal)))
      )
        (and 
          (>= total-votes quorum)
          (> (* (get votes-for proposal) u100) (* total-votes APPROVAL-THRESHOLD))
        )
      )
    false
  )
)

;; Get current quorum requirement for a proposal
(define-read-only (get-quorum-requirement (proposal-id uint))
  (match (map-get? proposals proposal-id)
    proposal (calculate-quorum (get total-supply-snapshot proposal))
    u0
  )
)

;; Get voting participation for a proposal
(define-read-only (get-participation (proposal-id uint))
  (match (map-get? proposals proposal-id)
    proposal 
      (let (
        (total-votes (+ (get votes-for proposal) (get votes-against proposal)))
        (total-supply (get total-supply-snapshot proposal))
      )
        {
          total-votes: total-votes,
          total-supply: total-supply,
          participation-percentage: (if (> total-supply u0)
            (/ (* total-votes u100) total-supply)
            u0
          ),
          quorum-met: (>= total-votes (calculate-quorum total-supply))
        }
      )
    { total-votes: u0, total-supply: u0, participation-percentage: u0, quorum-met: false }
  )
)

;; Get proposal status name
(define-read-only (get-status-name (status uint))
  (if (is-eq status STATUS-ACTIVE) "active"
    (if (is-eq status STATUS-PASSED) "passed"
      (if (is-eq status STATUS-REJECTED) "rejected"
        (if (is-eq status STATUS-EXECUTED) "executed"
          (if (is-eq status STATUS-EXPIRED) "expired"
            (if (is-eq status STATUS-CANCELLED) "cancelled"
              "unknown"
            )
          )
        )
      )
    )
  )
)

;; Get proposal type name
(define-read-only (get-type-name (proposal-type uint))
  (if (is-eq proposal-type TYPE-GENERAL) "general"
    (if (is-eq proposal-type TYPE-FUNDING) "funding"
      (if (is-eq proposal-type TYPE-PARAMETER) "parameter"
        (if (is-eq proposal-type TYPE-EMERGENCY) "emergency"
          "unknown"
        )
      )
    )
  )
)

;; =====================
;; GOVERNANCE PARAMETERS (read-only)
;; =====================

(define-read-only (get-governance-parameters)
  {
    voting-period: VOTING-PERIOD,
    timelock-period: TIMELOCK-PERIOD,
    min-proposal-tokens: MIN-PROPOSAL-TOKENS,
    quorum-percentage: QUORUM-PERCENTAGE,
    approval-threshold: APPROVAL-THRESHOLD
  }
)

Functions (28)

FunctionAccessArgs
get-token-balanceprivateaccount: principal
get-total-token-supplyprivate
calculate-voting-powerprivatevoter: principal
calculate-quorumprivatetotal-supply: uint
delegatepublicdelegate-to: principal
revoke-delegationpublic
create-proposalpublictitle: (string-ascii 100
cancel-proposalpublicproposal-id: uint
votepublicproposal-id: uint, vote-for: bool
change-votepublicproposal-id: uint, new-vote-for: bool
finalize-proposalpublicproposal-id: uint
execute-proposalpublicproposal-id: uint
get-proposalread-onlyproposal-id: uint
get-voteread-onlyproposal-id: uint, voter: principal
get-proposal-countread-only
is-proposal-activeread-onlyproposal-id: uint
is-in-timelockread-onlyproposal-id: uint
get-timelock-remainingread-onlyproposal-id: uint
get-voting-powerread-onlymember: principal
get-delegationread-onlydelegator: principal
get-delegated-powerread-onlydelegate-address: principal
has-delegated-powerread-onlydelegator: principal
would-proposal-passread-onlyproposal-id: uint
get-quorum-requirementread-onlyproposal-id: uint
get-participationread-onlyproposal-id: uint
get-status-nameread-onlystatus: uint
get-type-nameread-onlyproposal-type: uint
get-governance-parametersread-only