(use-trait ft-trait 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.sip-010-v1a.sip-010-trait)
;; stackswap governance
;;
;; Can see, vote and submit a new proposal
;; A proposal will just update the DAO with new contracts.
;; Errors
(define-constant ERR-NOT-ENOUGH-BALANCE u3401)
(define-constant ERR-NO-CONTRACT-CHANGES u3402)
(define-constant ERR-WRONG-TOKEN u3403)
(define-constant ERR-EMERGENCY-SHUTDOWN-ACTIVATED u3404)
(define-constant ERR-BLOCK-HEIGHT-NOT-REACHED u3405)
(define-constant ERR-NOT-AUTHORIZED u3406)
(define-constant STATUS-OK u0)
(define-constant PROPOSAL-IS-NOT-OPEN u3407)
(define-constant ERR-BLOCK-PASSED u3408)
(define-constant ERR-DAO-GET u3409)
;; Constants
(define-constant DAO-OWNER tx-sender)
;; Proposal variables
(define-map proposals
{ id: uint }
{
id: uint,
proposer: principal,
title: (string-utf8 256),
url: (string-utf8 256),
is-open: bool,
start-block-height: uint,
end-block-height: uint,
yes-votes: uint,
no-votes: uint,
contract-changes: (list 10 (tuple (name (string-ascii 256)) (address principal) (qualified-name principal) (can-mint bool) (can-burn bool)))
}
)
(define-data-var proposal-count uint u0)
(define-data-var proposal-ids (list 100 uint) (list u0))
(define-map votes-by-member { proposal-id: uint, member: principal } { vote-count: uint })
(define-map tokens-by-member { proposal-id: uint, member: principal, token: principal } { amount: uint })
;; Get all proposals
(define-read-only (get-proposals)
(ok (map get-proposal-by-id (var-get proposal-ids)))
)
;; Get all proposal IDs
(define-read-only (get-proposal-ids)
(ok (var-get proposal-ids))
)
;; Get votes for a member on proposal
(define-read-only (get-votes-by-member-by-id (proposal-id uint) (member principal))
(default-to
{ vote-count: u0 }
(map-get? votes-by-member { proposal-id: proposal-id, member: member })
)
)
(define-read-only (get-tokens-by-member-by-id (proposal-id uint) (member principal) (token <ft-trait>))
(default-to
{ amount: u0 }
(map-get? tokens-by-member { proposal-id: proposal-id, member: member, token: (contract-of token) })
)
)
;; Get proposal
(define-read-only (get-proposal-by-id (proposal-id uint))
(default-to
{
id: u0,
proposer: DAO-OWNER,
title: u"",
url: u"",
is-open: false,
start-block-height: u0,
end-block-height: u0,
yes-votes: u0,
no-votes: u0,
contract-changes: (list { name: "", address: DAO-OWNER, qualified-name: DAO-OWNER, can-mint: false, can-burn: false} )
}
(map-get? proposals { id: proposal-id })
)
)
;; To check which tokens are accepted as votes
(define-read-only (is-token-accepted (token <ft-trait>))
(let (
(is-stsw (is-eq (contract-of token) 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stsw-token-v3a))
(is-vstsw (is-eq (contract-of token) 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.vstsw-token-v1a))
)
(or is-stsw is-vstsw)
)
)
;; Start a proposal
;; Requires 1% of the supply in your wallet
;; Default voting period is 5 days (144 * 5 blocks) // templory 4 blocks
(define-public (propose
(start-block-height uint)
(title (string-utf8 256))
(url (string-utf8 256))
(contract-changes (list 10 (tuple (name (string-ascii 256)) (address principal) (qualified-name principal) (can-mint bool) (can-burn bool))))
)
(let (
(proposer-balance (unwrap-panic (contract-call? 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stsw-token-v3a get-balance tx-sender)))
(supply (- (unwrap-panic (contract-call? 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stsw-token-v3a get-total-supply)) u200000000))
(proposal-id (+ u1 (var-get proposal-count)))
)
(asserts! (<= block-height start-block-height) (err ERR-BLOCK-PASSED))
;; Requires 1% of the supply
(asserts! (>= (* proposer-balance u100) supply) (err ERR-NOT-ENOUGH-BALANCE))
;; Mutate
(asserts! (is-eq (unwrap! (contract-call? 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stackswap-dao-v4a get-qualified-name-by-name "governance") (err ERR-DAO-GET)) (as-contract tx-sender)) (err ERR-NOT-AUTHORIZED))
(map-set proposals
{ id: proposal-id }
{
id: proposal-id,
proposer: tx-sender,
title: title,
url: url,
is-open: true,
start-block-height: start-block-height,
end-block-height: (+ start-block-height u4),
yes-votes: u0,
no-votes: u0,
contract-changes: contract-changes
}
)
(var-set proposal-count proposal-id)
(var-set proposal-ids (unwrap-panic (as-max-len? (append (var-get proposal-ids) proposal-id) u100)))
(ok true)
)
)
(define-public (vote-for (token <ft-trait>) (proposal-id uint) (amount uint))
(let (
(proposal (get-proposal-by-id proposal-id))
(vote-count (get vote-count (get-votes-by-member-by-id proposal-id tx-sender)))
(token-count (get amount (get-tokens-by-member-by-id proposal-id tx-sender token)))
)
(asserts! (is-eq (is-token-accepted token) true) (err ERR-WRONG-TOKEN))
;; Proposal should be open for voting
(asserts! (is-eq (get is-open proposal) true) (err PROPOSAL-IS-NOT-OPEN))
;; Vote should be casted after the start-block-height
(asserts! (>= block-height (get start-block-height proposal)) (err ERR-NOT-AUTHORIZED))
;; Voter should be able to stake
(try! (contract-call? token transfer amount tx-sender (as-contract tx-sender) none))
;; Mutate
(map-set proposals
{ id: proposal-id }
(merge proposal { yes-votes: (+ amount (get yes-votes proposal)) }))
(map-set votes-by-member
{ proposal-id: proposal-id, member: tx-sender }
{ vote-count: (+ vote-count amount) })
(map-set tokens-by-member
{ proposal-id: proposal-id, member: tx-sender, token: (contract-of token) }
{ amount: (+ token-count amount) })
(ok true)
)
)
(define-public (vote-against (token <ft-trait>) (proposal-id uint) (amount uint))
(let (
(proposal (get-proposal-by-id proposal-id))
(vote-count (get vote-count (get-votes-by-member-by-id proposal-id tx-sender)))
(token-count (get amount (get-tokens-by-member-by-id proposal-id tx-sender token)))
)
(asserts! (is-eq (is-token-accepted token) true) (err ERR-WRONG-TOKEN))
;; Proposal should be open for voting
(asserts! (is-eq (get is-open proposal) true) (err PROPOSAL-IS-NOT-OPEN))
;; Vote should be casted after the start-block-height
(asserts! (>= block-height (get start-block-height proposal)) (err ERR-NOT-AUTHORIZED))
;; Voter should be able to stake
(try! (contract-call? token transfer amount tx-sender (as-contract tx-sender) none ))
;; Mutate
(map-set proposals
{ id: proposal-id }
(merge proposal { no-votes: (+ amount (get no-votes proposal)) }))
(map-set votes-by-member
{ proposal-id: proposal-id, member: tx-sender }
{ vote-count: (+ vote-count amount) })
(map-set tokens-by-member
{ proposal-id: proposal-id, member: tx-sender, token: (contract-of token) }
{ amount: (+ token-count amount) })
(ok true)
)
)
(define-public (end-proposal (proposal-id uint))
(let ((proposal (get-proposal-by-id proposal-id)))
(asserts! (not (is-eq (get id proposal) u0)) (err ERR-NOT-AUTHORIZED))
(asserts! (is-eq (get is-open proposal) true) (err PROPOSAL-IS-NOT-OPEN))
(asserts! (>= block-height (get end-block-height proposal)) (err ERR-BLOCK-HEIGHT-NOT-REACHED))
(map-set proposals
{ id: proposal-id }
(merge proposal { is-open: false }))
(if (> (get yes-votes proposal) (get no-votes proposal))
(try! (execute-proposal proposal-id))
false
)
(ok true)
)
)
;; Return votes to voter
(define-public (return-votes-to-member (token <ft-trait>) (proposal-id uint) (member principal))
(let (
(token-count (get amount (get-tokens-by-member-by-id proposal-id member token)))
(proposal (get-proposal-by-id proposal-id))
)
(asserts! (is-eq (is-token-accepted token) true) (err ERR-WRONG-TOKEN))
(asserts! (is-eq (get is-open proposal) false) (err PROPOSAL-IS-NOT-OPEN))
(asserts! (>= block-height (get end-block-height proposal)) (err ERR-NOT-AUTHORIZED))
(as-contract (contract-call? token transfer token-count (as-contract tx-sender) member none))
)
)
;; Make needed contract changes on DAO
(define-private (execute-proposal (proposal-id uint))
(let (
(proposal (get-proposal-by-id proposal-id))
(contract-changes (get contract-changes proposal))
)
(if (> (len contract-changes) u0)
(begin
(map execute-proposal-change-contract contract-changes)
(ok true)
)
(err ERR-NO-CONTRACT-CHANGES)
)
)
)
;; Helper to execute proposal and change contracts
(define-private (execute-proposal-change-contract (change (tuple (name (string-ascii 256)) (address principal) (qualified-name principal) (can-mint bool) (can-burn bool))))
(let (
(name (get name change))
(address (get address change))
(qualified-name (get qualified-name change))
(can-mint (get can-mint change))
(can-burn (get can-burn change))
)
(if (not (is-eq name ""))
(begin
(try! (contract-call? 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stackswap-dao-v4a set-contract-address name address qualified-name can-mint can-burn))
(ok true)
)
(ok false)
)
)
)
;; adds a new contract, only new ones allowed
(define-public (add-contract-address (name (string-ascii 256)) (address principal) (qualified-name principal) (can-mint bool) (can-burn bool))
(begin
(asserts! (is-eq tx-sender (contract-call? 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stackswap-dao-v4a get-dao-owner)) (err ERR-NOT-AUTHORIZED))
(if (is-some (contract-call? 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stackswap-dao-v4a get-contract-address-by-name name))
(ok false)
(begin
(try! (contract-call? 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stackswap-dao-v4a set-contract-address name address qualified-name can-mint can-burn))
(ok true)
)
)
)
)