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