Source Code

;; Fee Vault Contract
;; Collect fees, manage revenue sharing, handle withdrawals
;; Built with Clarity 4 features for Stacks Builder Challenge

;; ============================================
;; CONSTANTS & ERRORS
;; ============================================

(define-constant CONTRACT-OWNER tx-sender)
(define-constant ERR-NOT-AUTHORIZED (err u300))
(define-constant ERR-INSUFFICIENT-BALANCE (err u301))
(define-constant ERR-INVALID-PERCENTAGE (err u302))
(define-constant ERR-INVALID-AMOUNT (err u303))
(define-constant ERR-WITHDRAWAL-LOCKED (err u304))
(define-constant ERR-ALREADY-CLAIMED (err u305))
(define-constant ERR-NOT-STAKEHOLDER (err u306))
(define-constant ERR-ZERO-AMOUNT (err u307))
(define-constant ERR-TRANSFER-FAILED (err u308))

;; Revenue share percentages (basis points - 10000 = 100%)
(define-constant TREASURY-SHARE u7000)      ;; 70% to treasury
(define-constant STAKER-SHARE u2000)        ;; 20% to stakers  
(define-constant REFERRAL-SHARE u1000)      ;; 10% to referrers

(define-constant WITHDRAWAL-COOLDOWN u144)  ;; ~24 hours in blocks
(define-constant MIN-STAKE-AMOUNT u10000000) ;; 10 STX minimum stake

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

(define-data-var total-collected uint u0)
(define-data-var total-distributed uint u0)
(define-data-var treasury-balance uint u0)
(define-data-var staker-pool-balance uint u0)
(define-data-var referral-pool-balance uint u0)
(define-data-var total-staked uint u0)
(define-data-var distribution-epoch uint u0)
(define-data-var last-distribution-block uint u0)

;; ============================================
;; DATA MAPS
;; ============================================

;; Stakeholder information
(define-map stakers
  principal
  {
    staked-amount: uint,
    stake-block: uint,
    last-claim-epoch: uint,
    total-earned: uint,
    pending-rewards: uint
  }
)

;; Referral earnings tracking
(define-map referral-earnings
  principal
  {
    total-referrals: uint,
    total-earned: uint,
    pending-earnings: uint
  }
)

;; Withdrawal requests (for security)
(define-map withdrawal-requests
  principal
  {
    amount: uint,
    request-block: uint,
    is-pending: bool
  }
)

;; Epoch snapshots for reward calculation
(define-map epoch-snapshots
  uint  ;; epoch number
  {
    total-staked: uint,
    rewards-per-token: uint,
    distributed-at: uint
  }
)

;; Fee collection log
(define-map fee-log
  uint  ;; transaction counter
  {
    source: (string-ascii 32),
    amount: uint,
    block: uint
  }
)

(define-data-var fee-log-nonce uint u0)

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

;; Get vault balances
(define-read-only (get-balances)
  {
    total-collected: (var-get total-collected),
    total-distributed: (var-get total-distributed),
    treasury: (var-get treasury-balance),
    staker-pool: (var-get staker-pool-balance),
    referral-pool: (var-get referral-pool-balance),
    total-staked: (var-get total-staked)
  }
)

;; Get staker info
(define-read-only (get-staker (staker principal))
  (map-get? stakers staker)
)

;; Get staker's pending rewards
(define-read-only (get-pending-rewards (staker principal))
  (match (map-get? stakers staker)
    staker-data (get pending-rewards staker-data)
    u0
  )
)

;; Calculate reward share for a staker
(define-read-only (calculate-reward-share (staker principal))
  (let
    (
      (staker-data (default-to 
        { staked-amount: u0, stake-block: u0, last-claim-epoch: u0, total-earned: u0, pending-rewards: u0 }
        (map-get? stakers staker)))
      (staker-amount (get staked-amount staker-data))
      (total (var-get total-staked))
      (pool (var-get staker-pool-balance))
    )
    (if (or (is-eq total u0) (is-eq staker-amount u0))
      u0
      (/ (* pool staker-amount) total)
    )
  )
)

;; Get total staked
(define-read-only (get-total-staked)
  (var-get total-staked)
)

;; Get referral earnings
(define-read-only (get-referral-earnings (referrer principal))
  (map-get? referral-earnings referrer)
)

;; Get withdrawal request
(define-read-only (get-withdrawal-request (user principal))
  (map-get? withdrawal-requests user)
)

;; Check if withdrawal is ready
(define-read-only (is-withdrawal-ready (user principal))
  (match (map-get? withdrawal-requests user)
    request (and 
              (get is-pending request)
              (>= stacks-block-height (+ (get request-block request) WITHDRAWAL-COOLDOWN)))
    false
  )
)

;; Get current epoch
(define-read-only (get-current-epoch)
  (var-get distribution-epoch)
)

;; ============================================
;; PRIVATE FUNCTIONS
;; ============================================

;; Split incoming fee into pools
(define-private (split-fee (amount uint))
  (let
    (
      (treasury-amount (/ (* amount TREASURY-SHARE) u10000))
      (staker-amount (/ (* amount STAKER-SHARE) u10000))
      (referral-amount (/ (* amount REFERRAL-SHARE) u10000))
    )
    (var-set treasury-balance (+ (var-get treasury-balance) treasury-amount))
    (var-set staker-pool-balance (+ (var-get staker-pool-balance) staker-amount))
    (var-set referral-pool-balance (+ (var-get referral-pool-balance) referral-amount))
    
    { treasury: treasury-amount, stakers: staker-amount, referrals: referral-amount }
  )
)

;; Log fee collection
(define-private (log-fee (source (string-ascii 32)) (amount uint))
  (let
    (
      (nonce (+ (var-get fee-log-nonce) u1))
    )
    (map-set fee-log nonce
      {
        source: source,
        amount: amount,
        block: stacks-block-height
      }
    )
    (var-set fee-log-nonce nonce)
    nonce
  )
)

;; ============================================
;; PUBLIC FUNCTIONS
;; ============================================

;; Receive fee (called by other contracts or directly)
;; User transfers STX directly - we use contract-call pattern
(define-public (collect-fee (source (string-ascii 32)) (amount uint))
  (let
    (
      (payer tx-sender)
      (vault-addr (as-contract tx-sender))
    )
    (asserts! (> amount u0) ERR-ZERO-AMOUNT)
    
    ;; Transfer STX from payer to vault
    (try! (stx-transfer? amount payer vault-addr))
    
    ;; Update totals
    (var-set total-collected (+ (var-get total-collected) amount))
    
    ;; Split into pools
    (let
      (
        (split-result (split-fee amount))
        (log-id (log-fee source amount))
      )
      ;; Emit event for chainhook
      (print {
        event: "fee-collected",
        source: source,
        amount: amount,
        treasury-share: (get treasury split-result),
        staker-share: (get stakers split-result),
        referral-share: (get referrals split-result),
        log-id: log-id,
        block: stacks-block-height
      })
      
      (ok split-result)
    )
  )
)

;; Stake STX to earn rewards
(define-public (stake (amount uint))
  (let
    (
      (caller tx-sender)
      (vault-addr (as-contract tx-sender))
      (existing-stake (default-to 
        { staked-amount: u0, stake-block: u0, last-claim-epoch: u0, total-earned: u0, pending-rewards: u0 }
        (map-get? stakers caller)))
    )
    (asserts! (>= amount MIN-STAKE-AMOUNT) ERR-INVALID-AMOUNT)
    
    ;; Transfer STX to vault
    (try! (stx-transfer? amount caller vault-addr))
    
    ;; Update staker record
    (map-set stakers caller
      {
        staked-amount: (+ (get staked-amount existing-stake) amount),
        stake-block: (if (is-eq (get staked-amount existing-stake) u0) 
                        stacks-block-height 
                        (get stake-block existing-stake)),
        last-claim-epoch: (var-get distribution-epoch),
        total-earned: (get total-earned existing-stake),
        pending-rewards: (get pending-rewards existing-stake)
      }
    )
    
    ;; Update total staked
    (var-set total-staked (+ (var-get total-staked) amount))
    
    (print {
      event: "staked",
      staker: caller,
      amount: amount,
      total-staked: (+ (get staked-amount existing-stake) amount),
      block: stacks-block-height
    })
    
    (ok { staked: amount, total: (+ (get staked-amount existing-stake) amount) })
  )
)

;; Request unstake (starts cooldown)
(define-public (request-unstake (amount uint))
  (let
    (
      (caller tx-sender)
      (staker-data (unwrap! (map-get? stakers caller) ERR-NOT-STAKEHOLDER))
    )
    (asserts! (<= amount (get staked-amount staker-data)) ERR-INSUFFICIENT-BALANCE)
    (asserts! (> amount u0) ERR-ZERO-AMOUNT)
    
    ;; Create withdrawal request
    (map-set withdrawal-requests caller
      {
        amount: amount,
        request-block: stacks-block-height,
        is-pending: true
      }
    )
    
    (print {
      event: "unstake-requested",
      staker: caller,
      amount: amount,
      available-at: (+ stacks-block-height WITHDRAWAL-COOLDOWN),
      block: stacks-block-height
    })
    
    (ok { amount: amount, available-at: (+ stacks-block-height WITHDRAWAL-COOLDOWN) })
  )
)

;; Complete unstake after cooldown
(define-public (complete-unstake)
  (let
    (
      (caller tx-sender)
      (request (unwrap! (map-get? withdrawal-requests caller) ERR-WITHDRAWAL-LOCKED))
      (staker-data (unwrap! (map-get? stakers caller) ERR-NOT-STAKEHOLDER))
      (amount (get amount request))
    )
    ;; Check cooldown passed
    (asserts! (get is-pending request) ERR-WITHDRAWAL-LOCKED)
    (asserts! (>= stacks-block-height (+ (get request-block request) WITHDRAWAL-COOLDOWN)) ERR-WITHDRAWAL-LOCKED)
    
    ;; Transfer STX back to user
    (try! (as-contract (stx-transfer? amount tx-sender caller)))
    
    ;; Update staker record
    (map-set stakers caller
      (merge staker-data {
        staked-amount: (- (get staked-amount staker-data) amount)
      })
    )
    
    ;; Clear withdrawal request
    (map-set withdrawal-requests caller
      (merge request { is-pending: false })
    )
    
    ;; Update total staked
    (var-set total-staked (- (var-get total-staked) amount))
    
    (print {
      event: "unstake-completed",
      staker: caller,
      amount: amount,
      remaining-stake: (- (get staked-amount staker-data) amount),
      block: stacks-block-height
    })
    
    (ok amount)
  )
)

;; Claim staking rewards
(define-public (claim-rewards)
  (let
    (
      (caller tx-sender)
      (staker-data (unwrap! (map-get? stakers caller) ERR-NOT-STAKEHOLDER))
      (reward-amount (calculate-reward-share caller))
    )
    (asserts! (> reward-amount u0) ERR-ZERO-AMOUNT)
    
    ;; Transfer rewards
    (try! (as-contract (stx-transfer? reward-amount tx-sender caller)))
    
    ;; Update staker record
    (map-set stakers caller
      (merge staker-data {
        last-claim-epoch: (var-get distribution-epoch),
        total-earned: (+ (get total-earned staker-data) reward-amount),
        pending-rewards: u0
      })
    )
    
    ;; Update pool balance
    (var-set staker-pool-balance (- (var-get staker-pool-balance) reward-amount))
    (var-set total-distributed (+ (var-get total-distributed) reward-amount))
    
    (print {
      event: "rewards-claimed",
      staker: caller,
      amount: reward-amount,
      total-earned: (+ (get total-earned staker-data) reward-amount),
      block: stacks-block-height
    })
    
    (ok reward-amount)
  )
)

;; Credit referral earnings (called by registry)
(define-public (credit-referral (referrer principal) (amount uint))
  (let
    (
      (existing (default-to 
        { total-referrals: u0, total-earned: u0, pending-earnings: u0 }
        (map-get? referral-earnings referrer)))
    )
    ;; Only authorized contracts can credit referrals
    (asserts! (or (is-eq contract-caller .stackpulse-registry) (is-eq tx-sender CONTRACT-OWNER)) ERR-NOT-AUTHORIZED)
    
    (map-set referral-earnings referrer
      {
        total-referrals: (+ (get total-referrals existing) u1),
        total-earned: (get total-earned existing),
        pending-earnings: (+ (get pending-earnings existing) amount)
      }
    )
    
    (print {
      event: "referral-credited",
      referrer: referrer,
      amount: amount,
      block: stacks-block-height
    })
    
    (ok true)
  )
)

;; Claim referral earnings
(define-public (claim-referral-earnings)
  (let
    (
      (caller tx-sender)
      (earnings (unwrap! (map-get? referral-earnings caller) ERR-NOT-STAKEHOLDER))
      (pending (get pending-earnings earnings))
    )
    (asserts! (> pending u0) ERR-ZERO-AMOUNT)
    (asserts! (<= pending (var-get referral-pool-balance)) ERR-INSUFFICIENT-BALANCE)
    
    ;; Transfer earnings
    (try! (as-contract (stx-transfer? pending tx-sender caller)))
    
    ;; Update record
    (map-set referral-earnings caller
      (merge earnings {
        total-earned: (+ (get total-earned earnings) pending),
        pending-earnings: u0
      })
    )
    
    ;; Update pool
    (var-set referral-pool-balance (- (var-get referral-pool-balance) pending))
    (var-set total-distributed (+ (var-get total-distributed) pending))
    
    (print {
      event: "referral-earnings-claimed",
      referrer: caller,
      amount: pending,
      block: stacks-block-height
    })
    
    (ok pending)
  )
)

;; Withdraw treasury funds (admin only)
(define-public (withdraw-treasury (amount uint) (recipient principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (asserts! (<= amount (var-get treasury-balance)) ERR-INSUFFICIENT-BALANCE)
    
    (try! (as-contract (stx-transfer? amount tx-sender recipient)))
    
    (var-set treasury-balance (- (var-get treasury-balance) amount))
    (var-set total-distributed (+ (var-get total-distributed) amount))
    
    (print {
      event: "treasury-withdrawn",
      amount: amount,
      recipient: recipient,
      remaining: (- (var-get treasury-balance) amount),
      block: stacks-block-height
    })
    
    (ok amount)
  )
)

;; Trigger new distribution epoch (admin only)
(define-public (trigger-distribution)
  (let
    (
      (new-epoch (+ (var-get distribution-epoch) u1))
    )
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    
    ;; Create epoch snapshot
    (map-set epoch-snapshots new-epoch
      {
        total-staked: (var-get total-staked),
        rewards-per-token: (if (> (var-get total-staked) u0)
                            (/ (var-get staker-pool-balance) (var-get total-staked))
                            u0),
        distributed-at: stacks-block-height
      }
    )
    
    (var-set distribution-epoch new-epoch)
    (var-set last-distribution-block stacks-block-height)
    
    (print {
      event: "distribution-triggered",
      epoch: new-epoch,
      total-staked: (var-get total-staked),
      pool-balance: (var-get staker-pool-balance),
      block: stacks-block-height
    })
    
    (ok new-epoch)
  )
)

Functions (20)

FunctionAccessArgs
get-balancesread-only
get-stakerread-onlystaker: principal
get-pending-rewardsread-onlystaker: principal
calculate-reward-shareread-onlystaker: principal
get-total-stakedread-only
get-referral-earningsread-onlyreferrer: principal
get-withdrawal-requestread-onlyuser: principal
is-withdrawal-readyread-onlyuser: principal
get-current-epochread-only
split-feeprivateamount: uint
log-feeprivatesource: (string-ascii 32
collect-feepublicsource: (string-ascii 32
stakepublicamount: uint
request-unstakepublicamount: uint
complete-unstakepublic
claim-rewardspublic
credit-referralpublicreferrer: principal, amount: uint
claim-referral-earningspublic
withdraw-treasurypublicamount: uint, recipient: principal
trigger-distributionpublic