Source Code

;; proposal-engine.clar
;; Advanced proposal system with lifecycle, types, discussion, amendments
;; Clarity 4 / Epoch 3.3

;; -----------------------------------------------
;; Constants
;; -----------------------------------------------
(define-constant CONTRACT-OWNER tx-sender)
(define-constant ERR-NOT-AUTHORIZED (err u400))
(define-constant ERR-PROPOSAL-NOT-FOUND (err u401))
(define-constant ERR-INVALID-STATE (err u402))
(define-constant ERR-INVALID-TYPE (err u403))
(define-constant ERR-DISCUSSION-ACTIVE (err u404))
(define-constant ERR-NOT-PROPOSER (err u405))
(define-constant ERR-AMENDMENT-NOT-FOUND (err u406))
(define-constant ERR-INVALID-INPUT (err u407))
(define-constant ERR-ALREADY-VOTED (err u408))

;; Proposal types
(define-constant TYPE-TEXT u1)
(define-constant TYPE-PARAMETER u2)
(define-constant TYPE-CODE u3)
(define-constant TYPE-TREASURY u4)

;; Proposal states
(define-constant STATE-DRAFT u1)
(define-constant STATE-DISCUSSION u2)
(define-constant STATE-VOTING u3)
(define-constant STATE-EXECUTED u4)
(define-constant STATE-REJECTED u5)

;; -----------------------------------------------
;; Data Variables
;; -----------------------------------------------
(define-data-var proposal-nonce uint u0)
(define-data-var amendment-nonce uint u0)
(define-data-var discussion-period uint u72)
(define-data-var voting-period uint u144)
(define-data-var min-discussion-votes uint u3)

;; -----------------------------------------------
;; Data Maps
;; -----------------------------------------------
(define-map proposals
  uint
  {
    proposer: principal,
    title: (string-ascii 64),
    description: (string-ascii 256),
    proposal-type: uint,
    state: uint,
    created-at: uint,
    discussion-end: uint,
    voting-end: uint,
    yes-votes: uint,
    no-votes: uint,
    amendment-count: uint
  }
)

(define-map amendments
  uint
  {
    proposal-id: uint,
    author: principal,
    description: (string-ascii 256),
    accepted: bool,
    created-at: uint
  }
)

(define-map proposal-votes
  { proposal-id: uint, voter: principal }
  bool
)

(define-map discussion-comments
  { proposal-id: uint, comment-index: uint }
  {
    author: principal,
    content: (string-ascii 256),
    block: uint
  }
)

(define-map comment-counts
  uint
  uint
)

(define-map authorized-proposers
  principal
  bool
)

;; -----------------------------------------------
;; Initialize
;; -----------------------------------------------
(map-set authorized-proposers CONTRACT-OWNER true)

;; -----------------------------------------------
;; Private Functions
;; -----------------------------------------------
(define-private (is-valid-type (ptype uint))
  (or (is-eq ptype TYPE-TEXT)
      (is-eq ptype TYPE-PARAMETER)
      (is-eq ptype TYPE-CODE)
      (is-eq ptype TYPE-TREASURY))
)

(define-private (is-proposer (who principal))
  (default-to false (map-get? authorized-proposers who))
)

;; -----------------------------------------------
;; Public Functions
;; -----------------------------------------------

;; Authorize a proposer
(define-public (add-proposer (who principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (map-set authorized-proposers who true)
    (ok true)
  )
)

;; Create a new proposal (starts as draft)
(define-public (create-proposal
    (title (string-ascii 64))
    (description (string-ascii 256))
    (proposal-type uint))
  (let ((pid (var-get proposal-nonce)))
    (asserts! (is-proposer tx-sender) ERR-NOT-AUTHORIZED)
    (asserts! (is-valid-type proposal-type) ERR-INVALID-TYPE)
    (map-set proposals pid
      {
        proposer: tx-sender,
        title: title,
        description: description,
        proposal-type: proposal-type,
        state: STATE-DRAFT,
        created-at: tenure-height,
        discussion-end: u0,
        voting-end: u0,
        yes-votes: u0,
        no-votes: u0,
        amendment-count: u0
      })
    (map-set comment-counts pid u0)
    (var-set proposal-nonce (+ pid u1))
    (ok pid)
  )
)

;; Move proposal from draft to discussion
(define-public (start-discussion (proposal-id uint))
  (let ((proposal (unwrap! (map-get? proposals proposal-id)
                           ERR-PROPOSAL-NOT-FOUND)))
    (asserts! (is-eq tx-sender (get proposer proposal)) ERR-NOT-PROPOSER)
    (asserts! (is-eq (get state proposal) STATE-DRAFT) ERR-INVALID-STATE)
    (map-set proposals proposal-id
      (merge proposal {
        state: STATE-DISCUSSION,
        discussion-end: (+ tenure-height (var-get discussion-period)) }))
    (ok true)
  )
)

;; Add a discussion comment
(define-public (add-comment (proposal-id uint) (content (string-ascii 256)))
  (let (
    (proposal (unwrap! (map-get? proposals proposal-id)
                       ERR-PROPOSAL-NOT-FOUND))
    (idx (default-to u0 (map-get? comment-counts proposal-id)))
  )
    (asserts! (is-eq (get state proposal) STATE-DISCUSSION) ERR-INVALID-STATE)
    (asserts! (<= tenure-height (get discussion-end proposal))
              ERR-DISCUSSION-ACTIVE)
    (map-set discussion-comments
      { proposal-id: proposal-id, comment-index: idx }
      { author: tx-sender, content: content, block: tenure-height })
    (map-set comment-counts proposal-id (+ idx u1))
    (ok idx)
  )
)

;; Submit an amendment to a proposal
(define-public (submit-amendment
    (proposal-id uint)
    (description (string-ascii 256)))
  (let (
    (proposal (unwrap! (map-get? proposals proposal-id)
                       ERR-PROPOSAL-NOT-FOUND))
    (aid (var-get amendment-nonce))
  )
    (asserts! (is-eq (get state proposal) STATE-DISCUSSION) ERR-INVALID-STATE)
    (map-set amendments aid
      {
        proposal-id: proposal-id,
        author: tx-sender,
        description: description,
        accepted: false,
        created-at: tenure-height
      })
    (map-set proposals proposal-id
      (merge proposal {
        amendment-count: (+ (get amendment-count proposal) u1) }))
    (var-set amendment-nonce (+ aid u1))
    (ok aid)
  )
)

;; Accept an amendment (proposer only)
(define-public (accept-amendment (amendment-id uint))
  (let ((amendment (unwrap! (map-get? amendments amendment-id)
                            ERR-AMENDMENT-NOT-FOUND)))
    (let ((proposal (unwrap! (map-get? proposals (get proposal-id amendment))
                             ERR-PROPOSAL-NOT-FOUND)))
      (asserts! (is-eq tx-sender (get proposer proposal)) ERR-NOT-PROPOSER)
      (map-set amendments amendment-id
        (merge amendment { accepted: true }))
      (ok true)
    )
  )
)

;; Move proposal from discussion to voting
(define-public (start-voting (proposal-id uint))
  (let ((proposal (unwrap! (map-get? proposals proposal-id)
                           ERR-PROPOSAL-NOT-FOUND)))
    (asserts! (is-eq tx-sender (get proposer proposal)) ERR-NOT-PROPOSER)
    (asserts! (is-eq (get state proposal) STATE-DISCUSSION) ERR-INVALID-STATE)
    (asserts! (> tenure-height (get discussion-end proposal))
              ERR-DISCUSSION-ACTIVE)
    (map-set proposals proposal-id
      (merge proposal {
        state: STATE-VOTING,
        voting-end: (+ tenure-height (var-get voting-period)) }))
    (ok true)
  )
)

;; Cast vote on a proposal in voting state
(define-public (vote-on-proposal (proposal-id uint) (vote-for bool))
  (let ((proposal (unwrap! (map-get? proposals proposal-id)
                           ERR-PROPOSAL-NOT-FOUND)))
    (asserts! (is-eq (get state proposal) STATE-VOTING) ERR-INVALID-STATE)
    (asserts! (<= tenure-height (get voting-end proposal)) ERR-INVALID-STATE)
    (asserts! (is-none (map-get? proposal-votes
      { proposal-id: proposal-id, voter: tx-sender }))
      ERR-ALREADY-VOTED)
    (map-set proposal-votes
      { proposal-id: proposal-id, voter: tx-sender } vote-for)
    (if vote-for
      (map-set proposals proposal-id
        (merge proposal { yes-votes: (+ (get yes-votes proposal) u1) }))
      (map-set proposals proposal-id
        (merge proposal { no-votes: (+ (get no-votes proposal) u1) }))
    )
    (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)))
    (asserts! (is-eq (get state proposal) STATE-VOTING) ERR-INVALID-STATE)
    (asserts! (> tenure-height (get voting-end proposal)) ERR-INVALID-STATE)
    (if (> (get yes-votes proposal) (get no-votes proposal))
      (begin
        (map-set proposals proposal-id
          (merge proposal { state: STATE-EXECUTED }))
        (ok STATE-EXECUTED)
      )
      (begin
        (map-set proposals proposal-id
          (merge proposal { state: STATE-REJECTED }))
        (ok STATE-REJECTED)
      )
    )
  )
)

;; Update discussion period (admin only)
(define-public (set-discussion-period (blocks uint))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (asserts! (> blocks u0) ERR-INVALID-INPUT)
    (var-set discussion-period blocks)
    (ok true)
  )
)

;; -----------------------------------------------
;; Read-Only Functions
;; -----------------------------------------------
(define-read-only (get-proposal (proposal-id uint))
  (map-get? proposals proposal-id)
)

(define-read-only (get-amendment (amendment-id uint))
  (map-get? amendments amendment-id)
)

(define-read-only (get-comment (proposal-id uint) (index uint))
  (map-get? discussion-comments
    { proposal-id: proposal-id, comment-index: index })
)

(define-read-only (get-comment-count (proposal-id uint))
  (default-to u0 (map-get? comment-counts proposal-id))
)

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

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

(define-read-only (get-state-name (state uint))
  (if (is-eq state STATE-DRAFT) "draft"
    (if (is-eq state STATE-DISCUSSION) "discussion"
      (if (is-eq state STATE-VOTING) "voting"
        (if (is-eq state STATE-EXECUTED) "executed"
          (if (is-eq state STATE-REJECTED) "rejected"
            "unknown")))))
)

Functions (19)

FunctionAccessArgs
is-valid-typeprivateptype: uint
is-proposerprivatewho: principal
add-proposerpublicwho: principal
create-proposalpublictitle: (string-ascii 64
start-discussionpublicproposal-id: uint
add-commentpublicproposal-id: uint, content: (string-ascii 256
submit-amendmentpublicproposal-id: uint, description: (string-ascii 256
accept-amendmentpublicamendment-id: uint
start-votingpublicproposal-id: uint
vote-on-proposalpublicproposal-id: uint, vote-for: bool
finalize-proposalpublicproposal-id: uint
set-discussion-periodpublicblocks: uint
get-proposalread-onlyproposal-id: uint
get-amendmentread-onlyamendment-id: uint
get-commentread-onlyproposal-id: uint, index: uint
get-comment-countread-onlyproposal-id: uint
get-proposal-countread-only
get-voteread-onlyproposal-id: uint, voter: principal
get-state-nameread-onlystate: uint