Source Code

;; title: QuorumStack
;; version: 1.0.0
(define-trait sip-010-trait
  (
    (transfer (uint principal principal (optional (buff 34))) (response bool uint))
    (get-balance (principal) (response uint uint))
    (get-total-supply () (response uint uint))
    (get-name () (response (string-ascii 32) uint))
    (get-symbol () (response (string-ascii 10) uint))
    (get-decimals () (response uint uint))
    (get-token-uri () (response (optional (string-utf8 256)) uint))
  )
)

;; ============================================================
;;  Error Constants
;; ============================================================

(define-constant err-not-owner             (err u100))
(define-constant err-already-approved      (err u101))
(define-constant err-not-approved          (err u102))
(define-constant err-tx-not-found         (err u103))
(define-constant err-tx-expired           (err u104))
(define-constant err-tx-executed          (err u105))
(define-constant err-below-threshold      (err u106))
(define-constant err-invalid-threshold    (err u107))
(define-constant err-owner-exists         (err u108))
(define-constant err-insufficient-balance (err u109))
(define-constant err-self-approval        (err u110))
(define-constant err-tx-cancelled         (err u111))
(define-constant err-owner-not-found      (err u112))
(define-constant err-min-owners           (err u113))

(define-constant TX-TYPE-STX-TRANSFER     u1)
(define-constant TX-TYPE-TOKEN-TRANSFER   u2)
(define-constant TX-TYPE-ADD-OWNER        u3)
(define-constant TX-TYPE-REMOVE-OWNER     u4)
(define-constant TX-TYPE-CHANGE-THRESHOLD u5)

(define-data-var threshold uint u2)
(define-data-var owner-count uint u0)
(define-data-var tx-nonce uint u0)

(define-map owners principal bool)

(define-map transactions
  uint  ;; tx-id
  {
    proposer:        principal,
    tx-type:         uint,
    recipient:       (optional principal),
    amount:          (optional uint),
    memo:            (optional (buff 34)),
    token-contract:  (optional principal),
    new-principal:   (optional principal),
    new-value:       (optional uint),
    approval-count:  uint,
    executed:        bool,
    cancelled:       bool,
    expires-at:      uint
  }
)

(define-map approvals
  { tx-id: uint, owner: principal }
  bool
)

(map-set owners tx-sender true)
(var-set owner-count u1)

(define-private (assert-owner)
  (if (default-to false (map-get? owners tx-sender))
    (ok true)
    err-not-owner)
)

(define-private (get-tx-or-err (tx-id uint))
  (match (map-get? transactions tx-id)
    tx (ok tx)
    err-tx-not-found)
)

(define-private (assert-pending (tx { proposer: principal, tx-type: uint, recipient: (optional principal), amount: (optional uint), memo: (optional (buff 34)), token-contract: (optional principal), new-principal: (optional principal), new-value: (optional uint), approval-count: uint, executed: bool, cancelled: bool, expires-at: uint }))
  (if (get executed tx)
    err-tx-executed
    (if (get cancelled tx)
      err-tx-cancelled
      (if (>= block-height (get expires-at tx))
        err-tx-expired
        (ok true))))
)

(define-public (propose-transfer
    (recipient principal)
    (amount    uint)
    (memo      (optional (buff 34)))
    (expires-at uint))
  (begin
    (try! (assert-owner))
    (asserts! (> amount u0) (err u120))
    (asserts! (> expires-at block-height) err-tx-expired)
    (let ((tx-id (+ (var-get tx-nonce) u1)))
      (var-set tx-nonce tx-id)
      (map-set transactions tx-id {
        proposer:       tx-sender,
        tx-type:        TX-TYPE-STX-TRANSFER,
        recipient:      (some recipient),
        amount:         (some amount),
        memo:           memo,
        token-contract: none,
        new-principal:  none,
        new-value:      none,
        approval-count: u0,
        executed:       false,
        cancelled:      false,
        expires-at:     expires-at
      })
      (print { event: "propose-transfer", tx-id: tx-id, proposer: tx-sender, recipient: recipient, amount: amount, expires-at: expires-at })
      (ok tx-id)))
)

(define-public (propose-token-transfer
    (token-contract principal)
    (recipient      principal)
    (amount         uint)
    (memo           (optional (buff 34)))
    (expires-at     uint))
  (begin
    (try! (assert-owner))
    (asserts! (> amount u0) (err u120))
    (asserts! (> expires-at block-height) err-tx-expired)
    (let ((tx-id (+ (var-get tx-nonce) u1)))
      (var-set tx-nonce tx-id)
      (map-set transactions tx-id {
        proposer:       tx-sender,
        tx-type:        TX-TYPE-TOKEN-TRANSFER,
        recipient:      (some recipient),
        amount:         (some amount),
        memo:           memo,
        token-contract: (some token-contract),
        new-principal:  none,
        new-value:      none,
        approval-count: u0,
        executed:       false,
        cancelled:      false,
        expires-at:     expires-at
      })
      (print { event: "propose-token-transfer", tx-id: tx-id, proposer: tx-sender, token-contract: token-contract, recipient: recipient, amount: amount, expires-at: expires-at })
      (ok tx-id)))
)

(define-public (propose-add-owner
    (new-owner  principal)
    (expires-at uint))
  (begin
    (try! (assert-owner))
    (asserts! (not (default-to false (map-get? owners new-owner))) err-owner-exists)
    (asserts! (> expires-at block-height) err-tx-expired)
    (let ((tx-id (+ (var-get tx-nonce) u1)))
      (var-set tx-nonce tx-id)
      (map-set transactions tx-id {
        proposer:       tx-sender,
        tx-type:        TX-TYPE-ADD-OWNER,
        recipient:      none,
        amount:         none,
        memo:           none,
        token-contract: none,
        new-principal:  (some new-owner),
        new-value:      none,
        approval-count: u0,
        executed:       false,
        cancelled:      false,
        expires-at:     expires-at
      })
      (print { event: "propose-add-owner", tx-id: tx-id, proposer: tx-sender, new-owner: new-owner, expires-at: expires-at })
      (ok tx-id)))
)

(define-public (propose-remove-owner
    (owner      principal)
    (expires-at uint))
  (begin
    (try! (assert-owner))
    (asserts! (default-to false (map-get? owners owner)) err-owner-not-found)
    ;; Prevent removing last owner or making threshold unreachable
    (asserts! (> (var-get owner-count) u1) err-min-owners)
    (asserts! (> expires-at block-height) err-tx-expired)
    (let ((tx-id (+ (var-get tx-nonce) u1)))
      (var-set tx-nonce tx-id)
      (map-set transactions tx-id {
        proposer:       tx-sender,
        tx-type:        TX-TYPE-REMOVE-OWNER,
        recipient:      none,
        amount:         none,
        memo:           none,
        token-contract: none,
        new-principal:  (some owner),
        new-value:      none,
        approval-count: u0,
        executed:       false,
        cancelled:      false,
        expires-at:     expires-at
      })
      (print { event: "propose-remove-owner", tx-id: tx-id, proposer: tx-sender, owner: owner, expires-at: expires-at })
      (ok tx-id)))
)

(define-public (propose-change-threshold
    (new-threshold uint)
    (expires-at    uint))
  (begin
    (try! (assert-owner))
    (asserts! (> new-threshold u0) err-invalid-threshold)
    (asserts! (<= new-threshold (var-get owner-count)) err-invalid-threshold)
    (asserts! (> expires-at block-height) err-tx-expired)
    (let ((tx-id (+ (var-get tx-nonce) u1)))
      (var-set tx-nonce tx-id)
      (map-set transactions tx-id {
        proposer:       tx-sender,
        tx-type:        TX-TYPE-CHANGE-THRESHOLD,
        recipient:      none,
        amount:         none,
        memo:           none,
        token-contract: none,
        new-principal:  none,
        new-value:      (some new-threshold),
        approval-count: u0,
        executed:       false,
        cancelled:      false,
        expires-at:     expires-at
      })
      (print { event: "propose-change-threshold", tx-id: tx-id, proposer: tx-sender, new-threshold: new-threshold, expires-at: expires-at })
      (ok tx-id)))
)

(define-public (approve (tx-id uint))
  (begin
    (try! (assert-owner))
    (let ((tx (try! (get-tx-or-err tx-id))))
      (try! (assert-pending tx))
      ;; Self-approval prevention: proposer cannot be the first approver
      (asserts! (not (is-eq tx-sender (get proposer tx))) err-self-approval)
      ;; Double-approval prevention
      (asserts!
        (not (default-to false (map-get? approvals { tx-id: tx-id, owner: tx-sender })))
        err-already-approved)
      ;; Record approval
      (map-set approvals { tx-id: tx-id, owner: tx-sender } true)
      (let ((new-count (+ (get approval-count tx) u1)))
        (map-set transactions tx-id (merge tx { approval-count: new-count }))
        (print { event: "approved", tx-id: tx-id, owner: tx-sender, approval-count: new-count })
        (ok new-count))))
)

(define-public (revoke (tx-id uint))
  (begin
    (try! (assert-owner))
    (let ((tx (try! (get-tx-or-err tx-id))))
      (try! (assert-pending tx))
      ;; Must have already approved
      (asserts!
        (default-to false (map-get? approvals { tx-id: tx-id, owner: tx-sender }))
        err-not-approved)
      ;; Remove approval
      (map-delete approvals { tx-id: tx-id, owner: tx-sender })
      (let ((new-count (- (get approval-count tx) u1)))
        (map-set transactions tx-id (merge tx { approval-count: new-count }))
        (print { event: "revoked", tx-id: tx-id, owner: tx-sender, approval-count: new-count })
        (ok new-count))))
)

(define-public (execute (tx-id uint))
  (begin
    (try! (assert-owner))
    (let ((tx (try! (get-tx-or-err tx-id))))
      (try! (assert-pending tx))
      (asserts! (>= (get approval-count tx) (var-get threshold)) err-below-threshold)
      ;; Mark as executed immediately to prevent re-entrancy / double execution
      (map-set transactions tx-id (merge tx { executed: true }))
      (print { event: "executing", tx-id: tx-id, executor: tx-sender, tx-type: (get tx-type tx) })
      ;; Dispatch based on transaction type
      (if (is-eq (get tx-type tx) TX-TYPE-STX-TRANSFER)
        (execute-stx-transfer tx tx-id)
        (if (is-eq (get tx-type tx) TX-TYPE-TOKEN-TRANSFER)
          ;; Token transfers: caller must pass token contract as trait - see execute-token
          ;; For now, mark as executed and fire an event that the off-chain handler finishes
          (begin
            (print { event: "token-transfer-pending-finalization", tx-id: tx-id })
            (ok true))
          (if (is-eq (get tx-type tx) TX-TYPE-ADD-OWNER)
            (execute-add-owner tx tx-id)
            (if (is-eq (get tx-type tx) TX-TYPE-REMOVE-OWNER)
              (execute-remove-owner tx tx-id)
              (if (is-eq (get tx-type tx) TX-TYPE-CHANGE-THRESHOLD)
                (execute-change-threshold tx tx-id)
                (err u199))))))))
)

(define-public (execute-token (tx-id uint) (token <sip-010-trait>))
  (begin
    (try! (assert-owner))
    (let ((tx (try! (get-tx-or-err tx-id))))
      (asserts! (is-eq (get tx-type tx) TX-TYPE-TOKEN-TRANSFER) (err u198))
      (try! (assert-pending tx))
      (asserts! (>= (get approval-count tx) (var-get threshold)) err-below-threshold)
      ;; Verify the passed-in token contract matches what was proposed
      (asserts!
        (is-eq (some (contract-of token)) (get token-contract tx))
        (err u197))
      ;; Mark as executed first (reentrancy guard)
      (map-set transactions tx-id (merge tx { executed: true }))
      (let (
        (recipient (unwrap! (get recipient tx) (err u196)))
        (amount    (unwrap! (get amount tx)    (err u195)))
        (tx-memo   (get memo tx))
      )
        (try! (as-contract (contract-call? token transfer amount tx-sender recipient tx-memo)))
        (print { event: "token-transfer-executed", tx-id: tx-id, executor: tx-sender, recipient: recipient, amount: amount })
        (ok true))))
)

(define-private (execute-stx-transfer
    (tx { proposer: principal, tx-type: uint, recipient: (optional principal), amount: (optional uint), memo: (optional (buff 34)), token-contract: (optional principal), new-principal: (optional principal), new-value: (optional uint), approval-count: uint, executed: bool, cancelled: bool, expires-at: uint })
    (tx-id uint))
  (let (
    (recipient (unwrap! (get recipient tx) (err u194)))
    (amount    (unwrap! (get amount tx)    (err u193)))
  )
    (asserts! (<= amount (stx-get-balance (as-contract tx-sender))) err-insufficient-balance)
    (match (get memo tx)
      memo-bytes
        (try! (as-contract (stx-transfer-memo? amount tx-sender recipient memo-bytes)))
      ;; no memo
        (try! (as-contract (stx-transfer? amount tx-sender recipient))))
    (print { event: "stx-transfer-executed", tx-id: tx-id, recipient: recipient, amount: amount })
    (ok true))
)

(define-private (execute-add-owner
    (tx { proposer: principal, tx-type: uint, recipient: (optional principal), amount: (optional uint), memo: (optional (buff 34)), token-contract: (optional principal), new-principal: (optional principal), new-value: (optional uint), approval-count: uint, executed: bool, cancelled: bool, expires-at: uint })
    (tx-id uint))
  (let ((new-owner (unwrap! (get new-principal tx) (err u192))))
    ;; Guard: owner might have been added between proposal and execution
    (asserts! (not (default-to false (map-get? owners new-owner))) err-owner-exists)
    (map-set owners new-owner true)
    (var-set owner-count (+ (var-get owner-count) u1))
    (print { event: "owner-added", tx-id: tx-id, new-owner: new-owner, owner-count: (var-get owner-count) })
    (ok true))
)

(define-private (execute-remove-owner
    (tx { proposer: principal, tx-type: uint, recipient: (optional principal), amount: (optional uint), memo: (optional (buff 34)), token-contract: (optional principal), new-principal: (optional principal), new-value: (optional uint), approval-count: uint, executed: bool, cancelled: bool, expires-at: uint })
    (tx-id uint))
  (let (
    (owner     (unwrap! (get new-principal tx) (err u191)))
    (new-count (- (var-get owner-count) u1))
  )
    (asserts! (default-to false (map-get? owners owner)) err-owner-not-found)
    ;; After removal, threshold must remain reachable
    (asserts! (>= new-count (var-get threshold)) err-invalid-threshold)
    (asserts! (> new-count u0) err-min-owners)
    (map-delete owners owner)
    (var-set owner-count new-count)
    (print { event: "owner-removed", tx-id: tx-id, owner: owner, owner-count: new-count })
    (ok true))
)

(define-private (execute-change-threshold
    (tx { proposer: principal, tx-type: uint, recipient: (optional principal), amount: (optional uint), memo: (optional (buff 34)), token-contract: (optional principal), new-principal: (optional principal), new-value: (optional uint), approval-count: uint, executed: bool, cancelled: bool, expires-at: uint })
    (tx-id uint))
  (let ((new-threshold (unwrap! (get new-value tx) (err u190))))
    (asserts! (> new-threshold u0) err-invalid-threshold)
    (asserts! (<= new-threshold (var-get owner-count)) err-invalid-threshold)
    (var-set threshold new-threshold)
    (print { event: "threshold-changed", tx-id: tx-id, new-threshold: new-threshold })
    (ok true))
)

(define-read-only (get-transaction (tx-id uint))
  (map-get? transactions tx-id)
)

;;  has-approved
;;  Check whether a specific owner has approved a specific transaction
(define-read-only (has-approved (tx-id uint) (owner principal))
  (default-to false (map-get? approvals { tx-id: tx-id, owner: owner }))
)

;;  is-owner
;;  Check whether an address is a registered owner
(define-read-only (is-owner (address principal))
  (default-to false (map-get? owners address))
)

;;  get-threshold
;;  Returns the current quorum threshold
(define-read-only (get-threshold)
  (var-get threshold)
)

;;  get-owner-count
;;  Returns the total number of registered owners
(define-read-only (get-owner-count)
  (var-get owner-count)
)

;;  get-approval-count
;;  Returns the current number of approvals on a transaction
(define-read-only (get-approval-count (tx-id uint))
  (match (map-get? transactions tx-id)
    tx  (get approval-count tx)
    u0)
)

;;  get-balance
;;  Returns the contract's STX balance in microSTX
(define-read-only (get-balance)
  (stx-get-balance (as-contract tx-sender))
)

(define-read-only (get-tx-nonce)
  (var-get tx-nonce)
)

(define-data-var test-counter uint u0)

(define-public (increment-counter)
  (begin
    (var-set test-counter (+ (var-get test-counter) u1))
    (ok (var-get test-counter))
  )
)

(define-public (decrement-counter)
  (begin
    ;; Prevents underflow if already at 0
    (if (> (var-get test-counter) u0)
        (var-set test-counter (- (var-get test-counter) u1))
        true
    )
    (ok (var-get test-counter))
  )
)

(define-read-only (get-test-counter)
  (var-get test-counter)
)

Functions (22)

FunctionAccessArgs
assert-ownerprivate
get-tx-or-errprivatetx-id: uint
propose-transferpublicrecipient: principal, amount: uint, memo: (optional (buff 34
propose-token-transferpublictoken-contract: principal, recipient: principal, amount: uint, memo: (optional (buff 34
propose-add-ownerpublicnew-owner: principal, expires-at: uint
propose-remove-ownerpublicowner: principal, expires-at: uint
propose-change-thresholdpublicnew-threshold: uint, expires-at: uint
approvepublictx-id: uint
revokepublictx-id: uint
executepublictx-id: uint
execute-tokenpublictx-id: uint, token: <sip-010-trait>
get-transactionread-onlytx-id: uint
has-approvedread-onlytx-id: uint, owner: principal
is-ownerread-onlyaddress: principal
get-thresholdread-only
get-owner-countread-only
get-approval-countread-onlytx-id: uint
get-balanceread-only
get-tx-nonceread-only
increment-counterpublic
decrement-counterpublic
get-test-counterread-only