Source Code

;; @contract Arkadiko governance
;; Can see, vote and submit a new proposal
;; A proposal will just update the DAO with new contracts.
;; @version 3.1

(use-trait ft-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait)
(use-trait stake-pool-diko-trait .arkadiko-stake-pool-diko-trait-v1.stake-pool-diko-trait)

;; Errors
(define-constant ERR-NOT-ENOUGH-BALANCE u31)
(define-constant ERR-NO-CONTRACT-CHANGES u32)
(define-constant ERR-WRONG-TOKEN u33)
(define-constant ERR-EMERGENCY-SHUTDOWN-ACTIVATED u34)
(define-constant ERR-BLOCK-HEIGHT-NOT-REACHED u35)
(define-constant ERR-BLOCK-HEIGHT-PASSED u36)
(define-constant ERR-NOT-ENOUGH-PARTICIPATION u37)
(define-constant ERR-WRONG-POOL u38)
(define-constant ERR-VOTING-CLOSED u39)
(define-constant ERR-NOT-AUTHORIZED u3401)
(define-constant STATUS-OK u3200)

;; 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 governance-shutdown-activated bool false)
(define-data-var proposal-count uint u6)
(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-diko (is-eq (contract-of token) .arkadiko-token))
    (is-stdiko (is-eq (contract-of token) .stdiko-token))
  )
    (or is-diko is-stdiko)
  )
)

(define-public (toggle-governance-shutdown)
  (begin
    (asserts! (is-eq tx-sender (contract-call? .arkadiko-dao get-guardian-address)) (err ERR-NOT-AUTHORIZED))

    (ok (var-set governance-shutdown-activated (not (var-get governance-shutdown-activated))))
  )
)

(define-private (max-of (i1 uint) (i2 uint))
  (if (> i1 i2)
      i1
      i2))

(define-public (propose-dao
  (stake-pool-diko <stake-pool-diko-trait>)
  (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))))
)
  (propose
    stake-pool-diko
    start-block-height
    u250
    title
    url
    contract-changes
  )
)

;; @desc Start a proposal. Requires 1% of the supply in your wallet. Voting period is ~10 days.
;; @param stake-pool-diko; DIKO pool to get stDIKO/DIKO ratio from
;; @param start-block-height; block at which voting starts
;; @param title; title for the proposal
;; @param url; link to poposal details
;; @param contract-changes; contracts to update in DAO
;; @post boolean; returns true if propsal was created
(define-public (propose
  (stake-pool-diko <stake-pool-diko-trait>)
  (start-block-height uint)
  (vote-length 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-diko-balance (unwrap-panic (contract-call? .arkadiko-token get-balance tx-sender)))
    (proposer-stdiko-balance (unwrap-panic (contract-call? .stdiko-token get-balance tx-sender)))
    
    (proposer-diko-votes (unwrap-panic (token-amount-to-votes stake-pool-diko .arkadiko-token proposer-diko-balance)))
    (proposer-stdiko-votes (unwrap-panic (token-amount-to-votes stake-pool-diko .stdiko-token proposer-stdiko-balance)))
    (proposer-total-balance (+ proposer-diko-votes proposer-stdiko-votes))
    (end-block-height
      (if (is-eq tx-sender (contract-call? .arkadiko-dao get-dao-owner))
        (+ start-block-height (max-of u250 vote-length))
        (+ start-block-height u720)
      )
    )

    (diko-init-balance (unwrap-panic (contract-call? .arkadiko-token get-balance .arkadiko-diko-init)))
    (supply (- (unwrap-panic (contract-call? .arkadiko-token get-total-supply)) diko-init-balance))
    (proposal-id (+ u1 (var-get proposal-count)))
    (proposal {
      id: proposal-id,
      proposer: tx-sender,
      title: title,
      url: url,
      is-open: true,
      start-block-height: start-block-height,
      end-block-height: end-block-height,
      yes-votes: u0,
      no-votes: u0,
      contract-changes: contract-changes
    })
  )
    (asserts!
      (and
        (is-eq (unwrap-panic (contract-call? .arkadiko-dao get-emergency-shutdown-activated)) false)
        (is-eq (var-get governance-shutdown-activated) false)
      )
      (err ERR-EMERGENCY-SHUTDOWN-ACTIVATED)
    )
    (asserts!
      (is-eq
        (contract-of stake-pool-diko)
        (unwrap-panic (contract-call? .arkadiko-dao get-qualified-name-by-name "stake-pool-diko"))
      )
      (err ERR-WRONG-POOL)
    )
    (asserts! (>= start-block-height block-height) (err ERR-BLOCK-HEIGHT-PASSED))

    ;; Requires 0.25% of the supply 
    (asserts! (>= (* proposer-total-balance u25) supply) (err ERR-NOT-ENOUGH-BALANCE))
    ;; Mutate
    (map-set proposals
      { id: proposal-id }
      proposal
    )
    (var-set proposal-count proposal-id)
    (var-set proposal-ids (unwrap-panic (as-max-len? (append (var-get proposal-ids) proposal-id) u100)))
    (print { type: "proposal", action: "created", data: proposal })
    (ok true)
  )
)

;; @desc translate tokens to amount of votes - stDIKO to DIKO to votes
;; @param stake-pool-diko; DIKO stake pool to get DIKO/stDIKO ratio from
;; @param token; proposal to vote for
;; @param amount; amount of tokens
;; @post uint; amount of votes
(define-public (token-amount-to-votes (stake-pool-diko <stake-pool-diko-trait>) (token <ft-trait>) (amount uint))
  (begin
    (asserts! (is-eq (contract-of stake-pool-diko) (unwrap-panic (contract-call? .arkadiko-dao get-qualified-name-by-name "stake-pool-diko"))) (err ERR-NOT-AUTHORIZED))
    (if (is-eq (contract-of token) .arkadiko-token)
      (ok amount)
      (let (
        (diko-stdiko (unwrap-panic (contract-call? stake-pool-diko diko-stdiko-ratio)))
      )
        (ok (/ (* amount diko-stdiko) u1000000))
      )
    )
  )
)

;; @desc vote for a proposal
;; @param stake-pool-diko; DIKO stake pool to get DIKO/stDIKO ratio from
;; @param token; token used to vote (DIKO or stDIKO)
;; @param proposal-id; proposal to vote for
;; @param amount; amount of votes (tokens)
;; @post uint; returns 3200 when votes accepted
(define-public (vote-for (stake-pool-diko <stake-pool-diko-trait>) (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)))
    (add-vote-count (unwrap-panic (token-amount-to-votes stake-pool-diko token amount)))
  )
    (asserts!
      (and
        (is-eq (unwrap-panic (contract-call? .arkadiko-dao get-emergency-shutdown-activated)) false)
        (is-eq (var-get governance-shutdown-activated) false)
      )
      (err ERR-EMERGENCY-SHUTDOWN-ACTIVATED)
    )
    (asserts!
      (is-eq
        (contract-of stake-pool-diko)
        (unwrap-panic (contract-call? .arkadiko-dao get-qualified-name-by-name "stake-pool-diko"))
      )
      (err ERR-WRONG-POOL)
    )

    ;; Can vote with DIKO and stDIKO
    (asserts! (is-token-accepted token) (err ERR-WRONG-TOKEN))
    ;; Proposal should be open for voting
    (asserts! (get is-open proposal) (err ERR-VOTING-CLOSED))
    ;; Vote should be cast 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: (+ add-vote-count (get yes-votes proposal)) }))
    (map-set votes-by-member 
      { proposal-id: proposal-id, member: tx-sender }
      { vote-count: (+ vote-count add-vote-count) })
    (map-set tokens-by-member
      { proposal-id: proposal-id, member: tx-sender, token: (contract-of token) }
      { amount: (+ token-count amount) })

    (print { type: "proposal", action: "voted", data: proposal })
    (ok STATUS-OK)
  )
)

;; @desc vote against a proposal
;; @param stake-pool-diko; DIKO stake pool to get DIKO/stDIKO ratio from
;; @param token; token used to vote (DIKO or stDIKO)
;; @param proposal-id; proposal to vote against
;; @param amount; amount of votes (tokens)
;; @post uint; returns 3200 when votes accepted
(define-public (vote-against (stake-pool-diko <stake-pool-diko-trait>) (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)))
    (add-vote-count (unwrap-panic (token-amount-to-votes stake-pool-diko token amount)))
  )
    (asserts!
      (and
        (is-eq (unwrap-panic (contract-call? .arkadiko-dao get-emergency-shutdown-activated)) false)
        (is-eq (var-get governance-shutdown-activated) false)
      )
      (err ERR-EMERGENCY-SHUTDOWN-ACTIVATED)
    )
    (asserts!
      (is-eq
        (contract-of stake-pool-diko)
        (unwrap-panic (contract-call? .arkadiko-dao get-qualified-name-by-name "stake-pool-diko"))
      )
      (err ERR-WRONG-POOL)
    )

    ;; Can vote with DIKO and stDIKO
    (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 ERR-NOT-AUTHORIZED))
    ;; 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: (+ add-vote-count (get no-votes proposal)) }))
    (map-set votes-by-member 
      { proposal-id: proposal-id, member: tx-sender }
      { vote-count: (+ vote-count add-vote-count) })
    (map-set tokens-by-member
      { proposal-id: proposal-id, member: tx-sender, token: (contract-of token) }
      { amount: (+ token-count amount) })
    (print { type: "proposal", action: "voted", data: proposal })
    (ok STATUS-OK)
  )
)

;; @desc end a proposal and execute the changes
;; @param proposal-id; proposal to execute
;; @post uint; returns 3200 when executed
(define-public (end-proposal (proposal-id uint))
  (let (
    (proposal (get-proposal-by-id proposal-id))
    (diko-init-balance (unwrap-panic (contract-call? .arkadiko-token get-balance .arkadiko-diko-init)))
    (supply (- (unwrap-panic (contract-call? .arkadiko-token get-total-supply)) diko-init-balance))
    (sum-of-votes (+ (get no-votes proposal) (get yes-votes proposal)))
    (participation-threshold (/ (* u5 supply) u100))
  )
    (asserts!
      (and
        (is-eq (unwrap-panic (contract-call? .arkadiko-dao get-emergency-shutdown-activated)) false)
        (is-eq (var-get governance-shutdown-activated) false)
      )
      (err ERR-EMERGENCY-SHUTDOWN-ACTIVATED)
    )
    (asserts! (not (is-eq (get id proposal) u0)) (err ERR-NOT-AUTHORIZED))
    (asserts! (is-eq (get is-open proposal) true) (err ERR-NOT-AUTHORIZED))
    (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 (and (>= sum-of-votes participation-threshold) (> (get yes-votes proposal) (get no-votes proposal)))
      (try! (execute-proposal proposal-id))
      false
    )
    (print { type: "proposal", action: "ended", data: proposal })
    (ok STATUS-OK)
  )
)

;; @desc  Return votes (DIKO or stDIKO tokens) to voter
;; @param token; token to return (DIKO or stDIKO)
;; @param proposal-id; proposal for which to return tokens
;; @param member; voter to return tokens to
;; @post uint; returns result of token transfer from governance 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!
      (and
        (is-eq (unwrap-panic (contract-call? .arkadiko-dao get-emergency-shutdown-activated)) false)
        (is-eq (var-get governance-shutdown-activated) false)
      )
      (err ERR-EMERGENCY-SHUTDOWN-ACTIVATED)
    )
    (asserts! (is-eq (is-token-accepted token) true) (err ERR-WRONG-TOKEN))
    (asserts! (is-eq (get is-open proposal) false) (err ERR-NOT-AUTHORIZED))
    (asserts! (>= block-height (get end-block-height proposal)) (err ERR-NOT-AUTHORIZED))

    (map-set tokens-by-member
      { proposal-id: proposal-id, member: member, token: (contract-of token) }
      { amount: u0 })

    ;; Return DIKO or stDIKO
    (as-contract (contract-call? token transfer token-count 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)
        (print { type: "proposal", action: "executed", data: proposal })
        (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? .arkadiko-dao set-contract-address name address qualified-name can-mint can-burn))
        (ok true)
      )
      (ok false)
    )
  )
)

;; @desc  adds a new contract to the protocol, only new ones allowed
;; @param name; name for the new contract
;; @param address; address of the contract to add tot he DAO
;; @param qualified-name; qualified name for the new contract
;; @param can-mint; indication if new contract should be able to mint protocol tokens
;; @param can-burn; indication if new contract should be able to burn protocol tokens
;; @post boolean; returns true if contract added or false when contract already exists
(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? .arkadiko-dao get-dao-owner)) (err ERR-NOT-AUTHORIZED))

    (if (is-some (contract-call? .arkadiko-dao get-contract-address-by-name name))
      (ok false)
      (begin
        (try! (contract-call? .arkadiko-dao set-contract-address name address qualified-name can-mint can-burn))
        (ok true)
      )
    )
  )
)

Functions (18)

FunctionAccessArgs
get-proposalsread-only
get-proposal-idsread-only
get-votes-by-member-by-idread-onlyproposal-id: uint, member: principal
get-tokens-by-member-by-idread-onlyproposal-id: uint, member: principal, token: <ft-trait>
get-proposal-by-idread-onlyproposal-id: uint
is-token-acceptedread-onlytoken: <ft-trait>
toggle-governance-shutdownpublic
max-ofprivatei1: uint, i2: uint
propose-daopublicstake-pool-diko: <stake-pool-diko-trait>, start-block-height: uint, title: (string-utf8 256
proposepublicstake-pool-diko: <stake-pool-diko-trait>, start-block-height: uint, vote-length: uint, title: (string-utf8 256
token-amount-to-votespublicstake-pool-diko: <stake-pool-diko-trait>, token: <ft-trait>, amount: uint
vote-forpublicstake-pool-diko: <stake-pool-diko-trait>, token: <ft-trait>, proposal-id: uint, amount: uint
vote-againstpublicstake-pool-diko: <stake-pool-diko-trait>, token: <ft-trait>, proposal-id: uint, amount: uint
end-proposalpublicproposal-id: uint
return-votes-to-memberpublictoken: <ft-trait>, proposal-id: uint, member: principal
execute-proposalprivateproposal-id: uint
execute-proposal-change-contractprivatechange: (tuple (name (string-ascii 256
add-contract-addresspublicname: (string-ascii 256