Source Code

;; Trustless Token Faucet Contract
;; A fully decentralized faucet with streak-based rewards
;; No admin controls - trustless operation once deployed

;; === ERRORS ===
(define-constant err-cooldown-active (err u101))
(define-constant err-insufficient-faucet-balance (err u102))
(define-constant err-invalid-user (err u103))
(define-constant err-transfer-failed (err u104))
(define-constant err-invalid-deposit (err u105))

;; === CONSTANTS ===
(define-constant CONTRACT (as-contract tx-sender))
(define-constant COOLDOWN-BLOCKS u17280) ;; ~24 hours at 5 seconds per block
(define-constant STREAK-WINDOW u34560) ;; ~48 hours for streak maintenance
(define-constant TIER-1-REWARD u50000000) ;; 50M tokens (days 1-3)
(define-constant TIER-2-REWARD u75000000) ;; 75M tokens (days 4-7)
(define-constant TIER-3-REWARD u100000000) ;; 100M tokens (days 8-14)
(define-constant TIER-4-REWARD u125000000) ;; 125M tokens (days 15+)

;; === DATA MAPS ===
;; Track user claim data
(define-map user-claims
  principal
  {
    last-claim-block: uint,
    streak-count: uint,
    total-claims: uint,
    total-claimed: uint,
  }
)

;; Global statistics
(define-data-var total-distributed uint u0)

;; === PRIVATE FUNCTIONS ===

;; Calculate reward based on streak count
(define-private (calculate-reward (streak uint))
  (if (>= streak u15)
    TIER-4-REWARD
    (if (>= streak u8)
      TIER-3-REWARD
      (if (>= streak u4)
        TIER-2-REWARD
        TIER-1-REWARD
      )
    )
  )
)

;; Check if user can claim now
(define-private (is-valid-claim (user principal))
  (let (
      (current-block stacks-block-height)
      (user-data (default-to {
        last-claim-block: u0,
        streak-count: u0,
        total-claims: u0,
        total-claimed: u0,
      }
        (map-get? user-claims user)
      ))
    )
    (let (
        (last-claim-block (get last-claim-block user-data))
        (time-since-last (- current-block last-claim-block))
      )
      ;; Must wait for cooldown period
      (>= time-since-last COOLDOWN-BLOCKS)
    )
  )
)

;; Update user data after successful claim
(define-private (update-user-data
    (user principal)
    (reward-amount uint)
  )
  (let (
      (current-block stacks-block-height)
      (user-data (default-to {
        last-claim-block: u0,
        streak-count: u0,
        total-claims: u0,
        total-claimed: u0,
      }
        (map-get? user-claims user)
      ))
    )
    (let (
        (last-claim-block (get last-claim-block user-data))
        (current-streak (get streak-count user-data))
        (time-since-last (- current-block last-claim-block))
      )
      ;; Calculate new streak: increment if within window, reset to 1 if expired
      (let ((new-streak (if (or
            (is-eq last-claim-block u0)
            (> time-since-last STREAK-WINDOW)
          )
          u1 ;; Reset streak if first claim or window expired
          (+ current-streak u1)
        )))
        ;; Increment streak
        ;; Update user data
        (map-set user-claims user {
          last-claim-block: current-block,
          streak-count: new-streak,
          total-claims: (+ (get total-claims user-data) u1),
          total-claimed: (+ (get total-claimed user-data) reward-amount),
        })
        ;; Update global statistics
        (var-set total-distributed (+ (var-get total-distributed) reward-amount))
        ;; Log claim event for off-chain analytics
        (print {
          event: "claim",
          user: user,
          amount: reward-amount,
          streak: new-streak,
          block: current-block,
          total-claims: (+ (get total-claims user-data) u1),
        })
        new-streak
      )
    )
  )
)

;; === PUBLIC FUNCTIONS ===

;; Main claim function
(define-public (claim-tokens)
  (let ((claimer tx-sender))
    ;; Validate user can claim
    (asserts! (is-valid-claim claimer) err-cooldown-active)
    ;; Get user data to calculate reward
    (let ((user-data (default-to {
        last-claim-block: u0,
        streak-count: u0,
        total-claims: u0,
        total-claimed: u0,
      }
        (map-get? user-claims claimer)
      )))
      (let (
          (current-streak (get streak-count user-data))
          (last-claim-block (get last-claim-block user-data))
          (time-since-last (- stacks-block-height last-claim-block))
        )
        ;; Calculate new streak for reward calculation
        (let ((reward-streak (if (or
              (is-eq last-claim-block u0)
              (> time-since-last STREAK-WINDOW)
            )
            u1 ;; First claim or streak reset
            (+ current-streak u1)
          )))
          ;; Streak continues
          (let ((reward-amount (calculate-reward reward-streak)))
            ;; Check faucet has enough balance
            (let ((faucet-balance (unwrap-panic (contract-call? .tropical-blue-bonobo get-balance CONTRACT))))
              (asserts! (>= faucet-balance reward-amount)
                err-insufficient-faucet-balance
              )
              ;; Transfer tokens from faucet to claimer
              (match (as-contract (contract-call? .tropical-blue-bonobo transfer reward-amount tx-sender claimer
                none
              ))
                success (begin
                  ;; Update user data and log event
                  (update-user-data claimer reward-amount)
                  ;; Log streak milestone if applicable
                  (if (or
                      (is-eq reward-streak u4)
                      (is-eq reward-streak u8)
                      (is-eq reward-streak u15)
                    )
                    (print {
                      event: "streak_milestone",
                      user: claimer,
                      streak: reward-streak,
                      tier: (if (>= reward-streak u15)
                        u4
                        (if (>= reward-streak u8)
                          u3
                          (if (>= reward-streak u4)
                            u2
                            u1
                          )
                        )
                      ),
                    })
                    (print {
                      event: "streak_milestone",
                      user: claimer,
                      streak: reward-streak,
                      tier: u0,
                    })
                  )
                  (ok reward-amount)
                )
                error
                err-transfer-failed
              )
            )
          )
        )
      )
    )
  )
)

;; Deposit tokens to the faucet (anyone can contribute)
(define-public (deposit-tokens (amount uint))
  (begin
    (asserts! (>= amount u1) err-invalid-deposit)
    ;; Transfer tokens from sender to faucet contract
    (match (contract-call? .tropical-blue-bonobo transfer amount tx-sender CONTRACT none)
      success (begin
        ;; Log deposit event for off-chain analytics
        (print {
          event: "deposit",
          depositor: tx-sender,
          amount: amount,
          block: stacks-block-height,
        })
        (ok amount)
      )
      error
      err-transfer-failed
    )
  )
)

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

;; Get user's claim data
(define-read-only (get-claim-data (user principal))
  (let (
      (user-data (default-to {
        last-claim-block: u0,
        streak-count: u0,
        total-claims: u0,
        total-claimed: u0,
      }
        (map-get? user-claims user)
      ))
      (current-block stacks-block-height)
    )
    (let (
        (last-claim-block (get last-claim-block user-data))
        (time-since-last (- current-block last-claim-block))
        (can-claim (is-valid-claim user))
        (time-until-next (if can-claim
          u0
          (- COOLDOWN-BLOCKS time-since-last)
        ))
      )
      (merge user-data {
        can-claim-now: can-claim,
        time-until-next-claim: time-until-next,
        next-claim-block: (+ last-claim-block COOLDOWN-BLOCKS),
        current-reward: (calculate-reward (get streak-count user-data)),
      })
    )
  )
)

;; Check if user can claim right now
(define-read-only (can-claim-now (user principal))
  (is-valid-claim user)
)

;; Get current reward amount for user
(define-read-only (get-current-reward (user principal))
  (let ((user-data (default-to {
      last-claim-block: u0,
      streak-count: u0,
      total-claims: u0,
      total-claimed: u0,
    }
      (map-get? user-claims user)
    )))
    (calculate-reward (get streak-count user-data))
  )
)

;; Get reward amount for specific streak count
(define-read-only (get-reward-for-streak (streak uint))
  (calculate-reward streak)
)

;; Get when user can claim next (block height)
(define-read-only (get-next-claim-block (user principal))
  (let ((user-data (default-to {
      last-claim-block: u0,
      streak-count: u0,
      total-claims: u0,
      total-claimed: u0,
    }
      (map-get? user-claims user)
    )))
    (+ (get last-claim-block user-data) COOLDOWN-BLOCKS)
  )
)

;; Get faucet balance
(define-read-only (get-faucet-balance)
  (unwrap-panic (contract-call? .tropical-blue-bonobo get-balance CONTRACT))
)

;; Get global faucet statistics
(define-read-only (get-faucet-stats)
  {
    total-distributed: (var-get total-distributed),
    current-balance: (get-faucet-balance),
    cooldown-blocks: COOLDOWN-BLOCKS,
    streak-window-blocks: STREAK-WINDOW,
    current-block: stacks-block-height,
    tier-1-reward: TIER-1-REWARD,
    tier-2-reward: TIER-2-REWARD,
    tier-3-reward: TIER-3-REWARD,
    tier-4-reward: TIER-4-REWARD,
  }
)

Functions (11)

FunctionAccessArgs
calculate-rewardprivatestreak: uint
is-valid-claimprivateuser: principal
claim-tokenspublic
deposit-tokenspublicamount: uint
get-claim-dataread-onlyuser: principal
can-claim-nowread-onlyuser: principal
get-current-rewardread-onlyuser: principal
get-reward-for-streakread-onlystreak: uint
get-next-claim-blockread-onlyuser: principal
get-faucet-balanceread-only
get-faucet-statsread-only