Source Code

;; rewards-distributor.clar
;; Advanced rewards distribution system with merkle proofs and batch claims
;; Supports multiple reward epochs and token types

;; ============================================================================
;; Constants
;; ============================================================================

(define-constant CONTRACT-OWNER tx-sender)
(define-constant ERR-NOT-AUTHORIZED (err u401))
(define-constant ERR-EPOCH-NOT-FOUND (err u402))
(define-constant ERR-ALREADY-CLAIMED (err u403))
(define-constant ERR-INVALID-PROOF (err u404))
(define-constant ERR-EPOCH-NOT-ACTIVE (err u405))
(define-constant ERR-INSUFFICIENT-BALANCE (err u406))
(define-constant ERR-INVALID-AMOUNT (err u407))
(define-constant ERR-EPOCH-EXPIRED (err u408))
(define-constant ERR-MAX-EPOCHS-REACHED (err u409))
(define-constant ERR-INVALID-ROOT (err u410))

;; Configuration
(define-constant MAX-EPOCHS u1000)
(define-constant EPOCH-DURATION u4320)  ;; ~30 days in blocks
(define-constant CLAIM-WINDOW u8640)    ;; ~60 days to claim

;; ============================================================================
;; Data Variables
;; ============================================================================

(define-data-var current-epoch uint u0)
(define-data-var total-distributed uint u0)
(define-data-var treasury-balance uint u0)
(define-data-var distribution-paused bool false)

;; ============================================================================
;; Data Maps
;; ============================================================================

;; Epoch information
(define-map epochs uint
  {
    merkle-root: (buff 32),
    total-amount: uint,
    claimed-amount: uint,
    start-height: uint,
    end-height: uint,
    claim-deadline: uint,
    token-type: (string-ascii 32),
    is-active: bool,
    created-by: principal
  })

;; User claims per epoch
(define-map user-claims
  { epoch-id: uint, user: principal }
  { claimed: bool, amount: uint, claim-height: uint })

;; Aggregated user stats
(define-map user-stats principal
  {
    total-claimed: uint,
    epochs-claimed: uint,
    last-claim-height: uint,
    first-claim-height: uint
  })

;; Whitelist for reward distributors
(define-map distributors principal bool)

;; ============================================================================
;; Read-Only Functions
;; ============================================================================

(define-read-only (get-epoch (epoch-id uint))
  (map-get? epochs epoch-id))

(define-read-only (get-current-epoch)
  (var-get current-epoch))

(define-read-only (get-user-claim (epoch-id uint) (user principal))
  (map-get? user-claims { epoch-id: epoch-id, user: user }))

(define-read-only (has-claimed (epoch-id uint) (user principal))
  (match (map-get? user-claims { epoch-id: epoch-id, user: user })
    claim (get claimed claim)
    false))

(define-read-only (get-user-stats (user principal))
  (default-to
    { total-claimed: u0, epochs-claimed: u0, last-claim-height: u0, first-claim-height: u0 }
    (map-get? user-stats user)))

(define-read-only (is-distributor (account principal))
  (default-to false (map-get? distributors account)))

(define-read-only (get-epoch-status (epoch-id uint))
  (match (map-get? epochs epoch-id)
    epoch
      {
        exists: true,
        is-active: (get is-active epoch),
        is-expired: (> stacks-block-height (get claim-deadline epoch)),
        remaining-amount: (- (get total-amount epoch) (get claimed-amount epoch)),
        claim-rate: (if (is-eq (get total-amount epoch) u0)
                       u0
                       (/ (* (get claimed-amount epoch) u10000) (get total-amount epoch)))
      }
    { exists: false, is-active: false, is-expired: false, remaining-amount: u0, claim-rate: u0 }))

(define-read-only (get-claimable-epochs (user principal))
  ;; Returns list of epoch IDs user can claim from
  ;; Note: This is a simplified version - full implementation would iterate through epochs
  (list (var-get current-epoch)))

(define-read-only (get-treasury-info)
  {
    balance: (var-get treasury-balance),
    total-distributed: (var-get total-distributed),
    active-epochs: (var-get current-epoch),
    is-paused: (var-get distribution-paused)
  })

;; ============================================================================
;; Merkle Proof Verification
;; ============================================================================

;; Verify merkle proof for a claim
(define-read-only (verify-merkle-proof 
    (leaf (buff 32))
    (proof (list 20 (buff 32)))
    (root (buff 32)))
  (is-eq root (fold hash-pair proof leaf)))

;; Hash pair helper for merkle tree - compare buffers by first byte for ordering
(define-private (hash-pair (proof-element (buff 32)) (current-hash (buff 32)))
  (let (
    (current-first (element-at? current-hash u0))
    (proof-first (element-at? proof-element u0))
  )
    (if (is-some current-first)
      (if (is-some proof-first)
        (if (< (buff-to-uint-be (unwrap-panic current-first)) (buff-to-uint-be (unwrap-panic proof-first)))
          (keccak256 (concat current-hash proof-element))
          (keccak256 (concat proof-element current-hash)))
        (keccak256 (concat current-hash proof-element)))
      (keccak256 (concat current-hash proof-element)))))

;; Create leaf hash from claim data
(define-read-only (create-claim-leaf (user principal) (amount uint) (epoch-id uint))
  (keccak256 (concat 
    (concat (unwrap-panic (to-consensus-buff? user)) (unwrap-panic (to-consensus-buff? amount)))
    (unwrap-panic (to-consensus-buff? epoch-id)))))

;; ============================================================================
;; Distributor Management (Owner Only)
;; ============================================================================

(define-public (add-distributor (distributor principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (map-set distributors distributor true)
    (ok true)))

(define-public (remove-distributor (distributor principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (map-delete distributors distributor)
    (ok true)))

;; ============================================================================
;; Epoch Management
;; ============================================================================

;; Create new reward epoch
(define-public (create-epoch 
    (merkle-root (buff 32))
    (total-amount uint)
    (token-type (string-ascii 32)))
  (let
    (
      (epoch-id (+ (var-get current-epoch) u1))
      (start-height stacks-block-height)
      (end-height (+ stacks-block-height EPOCH-DURATION))
      (claim-deadline (+ stacks-block-height CLAIM-WINDOW))
    )
    (asserts! (or (is-eq tx-sender CONTRACT-OWNER) (is-distributor tx-sender)) ERR-NOT-AUTHORIZED)
    (asserts! (< epoch-id MAX-EPOCHS) ERR-MAX-EPOCHS-REACHED)
    (asserts! (> total-amount u0) ERR-INVALID-AMOUNT)
    (asserts! (> (len merkle-root) u0) ERR-INVALID-ROOT)
    
    ;; Create epoch
    (map-set epochs epoch-id
      {
        merkle-root: merkle-root,
        total-amount: total-amount,
        claimed-amount: u0,
        start-height: start-height,
        end-height: end-height,
        claim-deadline: claim-deadline,
        token-type: token-type,
        is-active: true,
        created-by: tx-sender
      })
    
    (var-set current-epoch epoch-id)
    
    (print {
      event: "epoch-created",
      epoch-id: epoch-id,
      total-amount: total-amount,
      merkle-root: merkle-root,
      start-height: start-height,
      claim-deadline: claim-deadline
    })
    
    (ok epoch-id)))

;; Deactivate epoch (stops new claims)
(define-public (deactivate-epoch (epoch-id uint))
  (let
    (
      (epoch (unwrap! (map-get? epochs epoch-id) ERR-EPOCH-NOT-FOUND))
    )
    (asserts! (or (is-eq tx-sender CONTRACT-OWNER) (is-eq tx-sender (get created-by epoch))) ERR-NOT-AUTHORIZED)
    
    (map-set epochs epoch-id (merge epoch { is-active: false }))
    
    (print { event: "epoch-deactivated", epoch-id: epoch-id })
    (ok true)))

;; ============================================================================
;; Claim Functions
;; ============================================================================

;; Claim rewards with merkle proof
(define-public (claim-rewards 
    (epoch-id uint)
    (amount uint)
    (proof (list 20 (buff 32))))
  (let
    (
      (epoch (unwrap! (map-get? epochs epoch-id) ERR-EPOCH-NOT-FOUND))
      (leaf (create-claim-leaf tx-sender amount epoch-id))
      (user-current-stats (get-user-stats tx-sender))
    )
    ;; Validations
    (asserts! (get is-active epoch) ERR-EPOCH-NOT-ACTIVE)
    (asserts! (<= stacks-block-height (get claim-deadline epoch)) ERR-EPOCH-EXPIRED)
    (asserts! (not (has-claimed epoch-id tx-sender)) ERR-ALREADY-CLAIMED)
    (asserts! (verify-merkle-proof leaf proof (get merkle-root epoch)) ERR-INVALID-PROOF)
    (asserts! (>= (- (get total-amount epoch) (get claimed-amount epoch)) amount) ERR-INSUFFICIENT-BALANCE)
    
    ;; Record claim
    (map-set user-claims
      { epoch-id: epoch-id, user: tx-sender }
      { claimed: true, amount: amount, claim-height: stacks-block-height })
    
    ;; Update epoch claimed amount
    (map-set epochs epoch-id 
      (merge epoch { claimed-amount: (+ (get claimed-amount epoch) amount) }))
    
    ;; Update user stats
    (map-set user-stats tx-sender
      {
        total-claimed: (+ (get total-claimed user-current-stats) amount),
        epochs-claimed: (+ (get epochs-claimed user-current-stats) u1),
        last-claim-height: stacks-block-height,
        first-claim-height: (if (is-eq (get first-claim-height user-current-stats) u0)
                               stacks-block-height
                               (get first-claim-height user-current-stats))
      })
    
    ;; Update totals
    (var-set total-distributed (+ (var-get total-distributed) amount))
    
    (print {
      event: "rewards-claimed",
      epoch-id: epoch-id,
      user: tx-sender,
      amount: amount,
      block: stacks-block-height
    })
    
    ;; Transfer rewards (in production, this would call stx-transfer? or token transfer)
    (ok amount)))

;; Batch claim from multiple epochs
(define-public (batch-claim 
    (claims (list 10 { epoch-id: uint, amount: uint, proof: (list 20 (buff 32)) })))
  (begin
    (asserts! (not (var-get distribution-paused)) ERR-NOT-AUTHORIZED)
    (ok (fold batch-claim-helper claims u0))))

(define-private (batch-claim-helper 
    (claim { epoch-id: uint, amount: uint, proof: (list 20 (buff 32)) })
    (total-claimed uint))
  (match (claim-single-reward (get epoch-id claim) (get amount claim) (get proof claim))
    amount (+ total-claimed amount)
    err total-claimed))

(define-private (claim-single-reward (epoch-id uint) (amount uint) (proof (list 20 (buff 32))))
  (let
    (
      (epoch (unwrap! (map-get? epochs epoch-id) (err u0)))
      (leaf (create-claim-leaf tx-sender amount epoch-id))
    )
    (if (and 
          (get is-active epoch)
          (<= stacks-block-height (get claim-deadline epoch))
          (not (has-claimed epoch-id tx-sender))
          (verify-merkle-proof leaf proof (get merkle-root epoch)))
      (begin
        (map-set user-claims
          { epoch-id: epoch-id, user: tx-sender }
          { claimed: true, amount: amount, claim-height: stacks-block-height })
        (map-set epochs epoch-id 
          (merge epoch { claimed-amount: (+ (get claimed-amount epoch) amount) }))
        (ok amount))
      (err u0))))

;; ============================================================================
;; Treasury Management
;; ============================================================================

;; Deposit to treasury
(define-public (deposit-to-treasury (amount uint))
  (begin
    (asserts! (> amount u0) ERR-INVALID-AMOUNT)
    ;; In production: (try! (stx-transfer? amount tx-sender (as-contract tx-sender)))
    (var-set treasury-balance (+ (var-get treasury-balance) amount))
    (print { event: "treasury-deposit", amount: amount, depositor: tx-sender })
    (ok (var-get treasury-balance))))

;; Withdraw unclaimed rewards after epoch expiry
(define-public (withdraw-expired-rewards (epoch-id uint))
  (let
    (
      (epoch (unwrap! (map-get? epochs epoch-id) ERR-EPOCH-NOT-FOUND))
      (unclaimed (- (get total-amount epoch) (get claimed-amount epoch)))
    )
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (asserts! (> stacks-block-height (get claim-deadline epoch)) ERR-EPOCH-NOT-ACTIVE)
    (asserts! (> unclaimed u0) ERR-INVALID-AMOUNT)
    
    ;; Mark epoch as fully claimed/expired
    (map-set epochs epoch-id 
      (merge epoch { 
        is-active: false,
        claimed-amount: (get total-amount epoch)
      }))
    
    (print { 
      event: "expired-rewards-withdrawn",
      epoch-id: epoch-id,
      amount: unclaimed 
    })
    
    (ok unclaimed)))

;; ============================================================================
;; Admin Functions
;; ============================================================================

;; Pause/unpause distribution
(define-public (set-distribution-paused (paused bool))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (var-set distribution-paused paused)
    (print { event: "distribution-paused", paused: paused })
    (ok paused)))

;; Emergency withdraw (owner only)
(define-public (emergency-withdraw (amount uint))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (asserts! (<= amount (var-get treasury-balance)) ERR-INSUFFICIENT-BALANCE)
    (var-set treasury-balance (- (var-get treasury-balance) amount))
    ;; In production: (try! (as-contract (stx-transfer? amount tx-sender CONTRACT-OWNER)))
    (print { event: "emergency-withdrawal", amount: amount })
    (ok amount)))

Functions (24)

FunctionAccessArgs
get-epochread-onlyepoch-id: uint
get-current-epochread-only
get-user-claimread-onlyepoch-id: uint, user: principal
has-claimedread-onlyepoch-id: uint, user: principal
get-user-statsread-onlyuser: principal
is-distributorread-onlyaccount: principal
get-epoch-statusread-onlyepoch-id: uint
get-claimable-epochsread-onlyuser: principal
get-treasury-inforead-only
verify-merkle-proofread-onlyleaf: (buff 32
hash-pairprivateproof-element: (buff 32
create-claim-leafread-onlyuser: principal, amount: uint, epoch-id: uint
add-distributorpublicdistributor: principal
remove-distributorpublicdistributor: principal
create-epochpublicmerkle-root: (buff 32
deactivate-epochpublicepoch-id: uint
claim-rewardspublicepoch-id: uint, amount: uint, proof: (list 20 (buff 32
batch-claimpublicclaims: (list 10 { epoch-id: uint, amount: uint, proof: (list 20 (buff 32
batch-claim-helperprivateclaim: { epoch-id: uint, amount: uint, proof: (list 20 (buff 32
claim-single-rewardprivateepoch-id: uint, amount: uint, proof: (list 20 (buff 32
deposit-to-treasurypublicamount: uint
withdraw-expired-rewardspublicepoch-id: uint
set-distribution-pausedpublicpaused: bool
emergency-withdrawpublicamount: uint