Source Code

;; =============================================================================
;; TREASURY CONTRACT
;; =============================================================================
;; Holds tokens deposited by the quests contract (creator commitments) and
;; supports withdrawals by quest creators and reward distribution to random
;; winners. Token balances are tracked per token contract.
;; =============================================================================

(use-trait token 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait)

;; =============================================================================
;; ERROR CODES
;; =============================================================================

(define-constant ERR_UNAUTHORIZED (err u2001))
(define-constant ERR_WRONG_TOKEN (err u2002))
;; Base index for transfer errors when rewarding winners (err = base + token index).
(define-constant ERR_TRANSFER_INDEX_PREFIX u1000)

;; =============================================================================
;; DATA VARIABLES
;; =============================================================================

(define-data-var treasury-owner principal tx-sender)

;; =============================================================================
;; MAPS
;; =============================================================================

;; token contract principal -> balance (uints)
(define-map token-balances
  principal
  uint
)

;; principal -> true if allowed to call reward-random-winners (treasury-owner is always allowed)
(define-map admins
  principal
  bool
)

;; =============================================================================
;; PUBLIC FUNCTIONS - Deposits and withdrawals
;; =============================================================================

;; Deposit tokens into the treasury. Anyone can deposit; typically called
;; by the quests contract when a creator creates a quest.

(define-public (deposit
    (amount uint)
    (sender principal)
    (token-contract <token>)
  )
  (let ((current-balance (default-to u0 (map-get? token-balances (contract-of token-contract)))))
    (asserts! (is-token-enabled (contract-of token-contract)) ERR_WRONG_TOKEN)
    (try! (restrict-assets? sender (
        (with-ft (contract-of token-contract) "*" amount)
        (with-stx amount)
      )
      (try! (contract-call? token-contract transfer amount sender current-contract none))
    ))
    (ok (map-set token-balances (contract-of token-contract)
      (+ current-balance amount)
    ))
  )
)

;; Withdraw tokens from the treasury. Only the quests contract can call
;; (e.g. when a quest creator cancels a quest).

(define-public (withdraw
    (amount uint)
    (recipient principal)
    (token-contract <token>)
  )
  (let (
      (token-principal (contract-of token-contract))
      (current-balance (default-to u0 (map-get? token-balances token-principal)))
    )
    (asserts! (is-eq contract-caller .quests) ERR_UNAUTHORIZED)
    (asserts! (is-token-enabled token-principal) ERR_WRONG_TOKEN)
    (try! (as-contract? ((with-ft token-principal "*" amount) (with-stx amount))
      (try! (contract-call? token-contract transfer amount tx-sender recipient none))
    ))
    (ok (map-set token-balances token-principal (- current-balance amount)))
  )
)

;; =============================================================================
;; PUBLIC FUNCTIONS - Admin
;; =============================================================================

;; Set the treasury owner. Only the current owner can call.

(define-public (set-treasury-owner (new-owner principal))
  (begin
    (asserts! (is-eq tx-sender contract-caller) ERR_UNAUTHORIZED)
    (asserts! (is-eq tx-sender (var-get treasury-owner)) ERR_UNAUTHORIZED)
    (ok (var-set treasury-owner new-owner))
  )
)

;; Add a principal to the admin list. Only the treasury owner can call.
(define-public (add-admin (admin principal))
  (begin
    (asserts! (is-eq tx-sender contract-caller) ERR_UNAUTHORIZED)
    (asserts! (is-eq tx-sender (var-get treasury-owner)) ERR_UNAUTHORIZED)
    (ok (map-set admins admin true))
  )
)

;; Remove a principal from the admin list. Only the treasury owner can call.
(define-public (remove-admin (admin principal))
  (begin
    (asserts! (is-eq tx-sender contract-caller) ERR_UNAUTHORIZED)
    (asserts! (is-eq tx-sender (var-get treasury-owner)) ERR_UNAUTHORIZED)
    (ok (map-delete admins admin))
  )
)

;; =============================================================================
;; PUBLIC FUNCTIONS - Rewards
;; =============================================================================

;; Distribute rewards to a list of winners across multiple tokens.
;; Treasury owner or any principal in the admin list can call. For each token,
;; 50% of balance is split among winners; the rest remains as platform balance.

(define-public (reward-random-winners
    (winners (list 100 principal))
    (tokens (list 100 <token>))
  )
  (begin
    (asserts! (is-eq tx-sender contract-caller) ERR_UNAUTHORIZED)
    (asserts!
      (or
        (is-eq tx-sender (var-get treasury-owner))
        (is-some (map-get? admins tx-sender))
      )
      ERR_UNAUTHORIZED
    )
    (match (fold process-token-winners tokens
      (ok {
        token-index: u0,
        winners: winners,
      })
    )
      result (ok true)
      err-result (err err-result)
    )
  )
)

;; =============================================================================
;; PRIVATE HELPERS - Reward distribution
;; =============================================================================

;; Transfers tokens to a single winner. Used as fold step; accumulator
;; is (response { index, token-contract, amount } uint).
;; #[allow(unchecked_data)]
(define-private (transfer-to-winner
    (winner principal)
    (result (response {
      index: uint,
      token-contract: <token>,
      amount: uint,
    }
      uint
    ))
  )
  (match result
    acc (let (
        (token-contract (get token-contract acc))
        (amount (get amount acc))
        (index (get index acc))
        (token-principal (contract-of token-contract))
      )
      (asserts! (is-token-enabled token-principal) ERR_WRONG_TOKEN)
      (try! (as-contract? ((with-ft token-principal "*" amount) (with-stx amount))
        (begin
          (unwrap!
            (contract-call? token-contract transfer amount tx-sender winner none)
            (err (+ ERR_TRANSFER_INDEX_PREFIX index))
          )
          true
        )))
      (ok {
        index: (+ index u1),
        token-contract: token-contract,
        amount: amount,
      })
    )
    err-index (err err-index)
  )
)

;; Processes all winners for one token: splits 50% of token balance among
;; winners. Accumulator: (response { token-index, winners } uint).
;; #[allow(unchecked_data)]
(define-private (process-token-winners
    (token-contract <token>)
    (result (response {
      token-index: uint,
      winners: (list 100 principal),
    }
      uint
    ))
  )
  (match result
    acc (let (
        (token-principal (contract-of token-contract))
        (balance (default-to u0 (map-get? token-balances token-principal)))
        (fee (/ balance u2))
        (winners (get winners acc))
        (winners-count (len winners))
        (token-index (get token-index acc))
      )
      (if (is-eq winners-count u0)
        (ok {
          token-index: (+ token-index u1),
          winners: winners,
        })
        (if (is-eq (/ fee winners-count) u0)
          (ok {
            token-index: (+ token-index u1),
            winners: winners,
          })
          (let ((amount-per-winner (/ fee winners-count)))
            (match (fold transfer-to-winner winners
              (ok {
                index: u0,
                token-contract: token-contract,
                amount: amount-per-winner,
              })
            )
              transfer-result (begin
                (try! (as-contract? ((with-ft token-principal "*" fee) (with-stx fee))
                  (try! (contract-call? token-contract transfer fee tx-sender
                    (var-get treasury-owner) none
                  ))
                ))
                (map-set token-balances token-principal (- balance balance))
                (ok {
                  token-index: (+ token-index u1),
                  winners: winners,
                })
              )
              err-transfer (err err-transfer)
            )
          )
        )
      )
    )
    err-index (err err-index)
  )
)

;; =============================================================================
;; READ-ONLY FUNCTIONS
;; =============================================================================

(define-read-only (get-balance (token-principal principal))
  (default-to u0 (map-get? token-balances token-principal))
)

(define-read-only (get-treasury-owner)
  (var-get treasury-owner)
)

(define-read-only (is-admin (who principal))
  (is-some (map-get? admins who))
)

;; =============================================================================
;; PRIVATE HELPERS - Token validation
;; =============================================================================

;; Checks token against whitelist (ZADAO-token-whitelist-v2).
;; #[allow(unchecked_data)]
(define-private (is-token-enabled (token-id principal))
  (contract-call?
    .ZADAO-token-whitelist-v2
    is-token-enabled token-id
  )
)

Functions (9)

FunctionAccessArgs
set-treasury-ownerpublicnew-owner: principal
add-adminpublicadmin: principal
remove-adminpublicadmin: principal
reward-random-winnerspublicwinners: (list 100 principal
transfer-to-winnerprivatewinner: principal, result: (response { index: uint, token-contract: <token>, amount: uint, } uint
get-balanceread-onlytoken-principal: principal
get-treasury-ownerread-only
is-adminread-onlywho: principal
is-token-enabledprivatetoken-id: principal