Source Code

;; Staking Rewards Contract
;; Provides TLX token rewards for long-term position holders
;; Rewards are paid in TLX tokens (minted on claim)
;; Uses Clarity 4 features

(define-constant contract-owner tx-sender)
(define-constant err-owner-only (err u100))
(define-constant err-not-authorized (err u101))
(define-constant err-invalid-amount (err u102))
(define-constant err-insufficient-balance (err u103))
(define-constant err-position-not-found (err u104))
(define-constant err-already-staked (err u105))
(define-constant err-not-staked (err u106))
(define-constant err-min-lock-not-met (err u107))
(define-constant err-rewards-not-available (err u108))
(define-constant err-cooldown-active (err u109))
(define-constant err-token-mint-failed (err u110))

;; TLX Token contract reference (must be set after deployment)
(define-data-var tlx-token-contract principal contract-owner)

;; Staking configuration
(define-data-var reward-rate-per-block uint u100) ;; Rewards per block per 1M STX staked
(define-data-var min-stake-amount uint u100000) ;; 0.1 STX minimum
(define-data-var min-lock-period uint u10080) ;; ~7 days in blocks
(define-data-var reward-pool-balance uint u0)
(define-data-var total-staked uint u0)
(define-data-var cooldown-period uint u1440) ;; ~1 day cooldown for unstaking

;; Reward tiers based on lock duration
(define-map reward-multipliers
  { tier: uint }
  { multiplier: uint, min-blocks: uint, name: (string-ascii 32) }
)

;; Staker data
(define-map stakers
  { address: principal }
  {
    staked-amount: uint,
    stake-start-block: uint,
    last-reward-block: uint,
    accumulated-rewards: uint,
    tier: uint,
    is-active: bool,
    cooldown-start: (optional uint)
  }
)

;; Staking history for analytics
(define-map staking-history
  { address: principal, index: uint }
  {
    action: (string-ascii 16),
    amount: uint,
    block-height: uint,
    rewards-claimed: uint
  }
)

(define-map user-history-count
  { address: principal }
  { count: uint }
)

;; Initialize reward tiers
(map-set reward-multipliers { tier: u1 } { multiplier: u100, min-blocks: u10080, name: "Bronze" })
(map-set reward-multipliers { tier: u2 } { multiplier: u150, min-blocks: u43200, name: "Silver" })
(map-set reward-multipliers { tier: u3 } { multiplier: u200, min-blocks: u129600, name: "Gold" })
(map-set reward-multipliers { tier: u4 } { multiplier: u300, min-blocks: u259200, name: "Platinum" })
(map-set reward-multipliers { tier: u5 } { multiplier: u500, min-blocks: u525600, name: "Diamond" })

;; Read-only functions

;; Get current reward rate
(define-read-only (get-reward-rate)
  (var-get reward-rate-per-block)
)

;; Get staker info
(define-read-only (get-staker-info (address principal))
  (map-get? stakers { address: address })
)

;; Get tier info
(define-read-only (get-tier-info (tier uint))
  (map-get? reward-multipliers { tier: tier })
)

;; Calculate pending rewards for a staker
(define-read-only (calculate-pending-rewards (address principal))
  (match (map-get? stakers { address: address })
    staker-data
      (if (get is-active staker-data)
        (let (
          (blocks-staked (- stacks-block-height (get last-reward-block staker-data)))
          (staked-amount (get staked-amount staker-data))
          (tier-data (unwrap! (map-get? reward-multipliers { tier: (get tier staker-data) }) u0))
          (base-reward (/ (* staked-amount (var-get reward-rate-per-block) blocks-staked) u1000000))
          (multiplied-reward (/ (* base-reward (get multiplier tier-data)) u100))
        )
          multiplied-reward
        )
        u0
      )
    u0
  )
)

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

;; Get reward pool balance
(define-read-only (get-reward-pool)
  (var-get reward-pool-balance)
)

;; Calculate APY based on current parameters
(define-read-only (calculate-apy (tier uint))
  (match (map-get? reward-multipliers { tier: tier })
    tier-data
      (let (
        (blocks-per-year u525600) ;; ~1 year in blocks (10 min blocks)
        (base-rate (var-get reward-rate-per-block))
        (yearly-rate (* base-rate blocks-per-year))
        (multiplied-rate (/ (* yearly-rate (get multiplier tier-data)) u100))
      )
        multiplied-rate
      )
    u0
  )
)

;; Determine tier based on lock duration
(define-read-only (determine-tier (blocks-locked uint))
  (if (>= blocks-locked u525600)
    u5
    (if (>= blocks-locked u259200)
      u4
      (if (>= blocks-locked u129600)
        u3
        (if (>= blocks-locked u43200)
          u2
          u1
        )
      )
    )
  )
)

;; Check if user is in cooldown
(define-read-only (is-in-cooldown (address principal))
  (match (map-get? stakers { address: address })
    staker-data
      (match (get cooldown-start staker-data)
        cooldown-block
          (< (- stacks-block-height cooldown-block) (var-get cooldown-period))
        false
      )
    false
  )
)

;; Get staking history for user
(define-read-only (get-staking-history (address principal) (index uint))
  (map-get? staking-history { address: address, index: index })
)

;; Public functions

;; Stake STX tokens
(define-public (stake (amount uint) (lock-duration uint))
  (let (
    (existing-stake (map-get? stakers { address: tx-sender }))
  )
    ;; Validate amount
    (asserts! (>= amount (var-get min-stake-amount)) err-invalid-amount)
    ;; Validate lock duration
    (asserts! (>= lock-duration (var-get min-lock-period)) err-min-lock-not-met)
    ;; Check if already staked
    (asserts! (is-none existing-stake) err-already-staked)
    
    ;; Transfer STX to contract
    (try! (stx-transfer? amount tx-sender current-contract))
    
    ;; Calculate tier based on lock duration
    (let (
      (tier (determine-tier lock-duration))
    )
      ;; Create staker record
      (map-set stakers
        { address: tx-sender }
        {
          staked-amount: amount,
          stake-start-block: stacks-block-height,
          last-reward-block: stacks-block-height,
          accumulated-rewards: u0,
          tier: tier,
          is-active: true,
          cooldown-start: none
        }
      )
      
      ;; Update total staked
      (var-set total-staked (+ (var-get total-staked) amount))
      
      ;; Record history
      (record-history tx-sender "stake" amount u0)
      
      (ok { staked: amount, tier: tier })
    )
  )
)

;; Add to existing stake
(define-public (add-stake (amount uint))
  (let (
    (staker-data (unwrap! (map-get? stakers { address: tx-sender }) err-not-staked))
  )
    ;; Validate amount
    (asserts! (> amount u0) err-invalid-amount)
    ;; Must be active staker
    (asserts! (get is-active staker-data) err-not-staked)
    
    ;; First claim any pending rewards
    (try! (claim-rewards))
    
    ;; Transfer additional STX
    (try! (stx-transfer? amount tx-sender current-contract))
    
    ;; Update staker record
    (map-set stakers
      { address: tx-sender }
      (merge staker-data {
        staked-amount: (+ (get staked-amount staker-data) amount)
      })
    )
    
    ;; Update total staked
    (var-set total-staked (+ (var-get total-staked) amount))
    
    ;; Record history
    (record-history tx-sender "add-stake" amount u0)
    
    (ok (+ (get staked-amount staker-data) amount))
  )
)

;; Claim accumulated rewards
(define-public (claim-rewards)
  (let (
    (staker-data (unwrap! (map-get? stakers { address: tx-sender }) err-not-staked))
    (pending (calculate-pending-rewards tx-sender))
  )
    ;; Must be active staker
    (asserts! (get is-active staker-data) err-not-staked)
    ;; Must have rewards to claim
    (asserts! (> pending u0) err-rewards-not-available)
    
    ;; Mint TLX tokens as reward (instead of STX transfer)
    (try! (contract-call? 'SP5K2RHMSBH4PAP4PGX77MCVNK1ZEED07CWX9TJT.timelock-token-v11-1 mint pending tx-sender))
    
    ;; Update staker record
    (map-set stakers
      { address: tx-sender }
      (merge staker-data {
        last-reward-block: stacks-block-height,
        accumulated-rewards: (+ (get accumulated-rewards staker-data) pending)
      })
    )
    
    ;; Update reward pool
    (var-set reward-pool-balance (- (var-get reward-pool-balance) pending))
    
    ;; Record history
    (record-history tx-sender "claim" u0 pending)
    
    (ok pending)
  )
)

;; Initiate unstake (starts cooldown)
(define-public (initiate-unstake)
  (let (
    (staker-data (unwrap! (map-get? stakers { address: tx-sender }) err-not-staked))
  )
    ;; Must be active staker
    (asserts! (get is-active staker-data) err-not-staked)
    ;; Must not already be in cooldown
    (asserts! (is-none (get cooldown-start staker-data)) err-cooldown-active)
    
    ;; Set cooldown start
    (map-set stakers
      { address: tx-sender }
      (merge staker-data {
        cooldown-start: (some stacks-block-height)
      })
    )
    
    (ok stacks-block-height)
  )
)

;; Complete unstake after cooldown
(define-public (complete-unstake)
  (let (
    (user tx-sender)
    (staker-data (unwrap! (map-get? stakers { address: tx-sender }) err-not-staked))
    (cooldown-start (unwrap! (get cooldown-start staker-data) err-cooldown-active))
  )
    ;; Must be active staker
    (asserts! (get is-active staker-data) err-not-staked)
    ;; Cooldown must have passed
    (asserts! (>= (- stacks-block-height cooldown-start) (var-get cooldown-period)) err-cooldown-active)
    
    ;; Claim any remaining rewards first
    (let (
      (pending (calculate-pending-rewards user))
      (staked-amount (get staked-amount staker-data))
    )
      ;; Transfer staked amount back (STX)
      (try! (as-contract? ((with-stx staked-amount)) (try! (stx-transfer? staked-amount tx-sender user))))
      
      ;; Mint any pending TLX rewards if available
      (and (> pending u0)
        (is-ok (contract-call? 'SP5K2RHMSBH4PAP4PGX77MCVNK1ZEED07CWX9TJT.timelock-token-v11-1 mint pending user)))
      
      ;; Update staker record
      (map-set stakers
        { address: user }
        (merge staker-data {
          staked-amount: u0,
          is-active: false,
          cooldown-start: none
        })
      )
      
      ;; Update total staked
      (var-set total-staked (- (var-get total-staked) staked-amount))
      
      ;; Record history
      (record-history user "unstake" staked-amount pending)
      
      (ok { unstaked: staked-amount, rewards: pending })
    )
  )
)

;; Cancel unstake cooldown
(define-public (cancel-unstake)
  (let (
    (staker-data (unwrap! (map-get? stakers { address: tx-sender }) err-not-staked))
  )
    ;; Must have active cooldown
    (asserts! (is-some (get cooldown-start staker-data)) err-cooldown-active)
    
    ;; Clear cooldown
    (map-set stakers
      { address: tx-sender }
      (merge staker-data {
        cooldown-start: none
      })
    )
    
    (ok true)
  )
)

;; Admin: Fund reward pool
(define-public (fund-reward-pool (amount uint))
  (begin
    (asserts! (is-eq tx-sender contract-owner) err-owner-only)
    (asserts! (> amount u0) err-invalid-amount)
    
    (try! (stx-transfer? amount tx-sender current-contract))
    (var-set reward-pool-balance (+ (var-get reward-pool-balance) amount))
    
    (ok (var-get reward-pool-balance))
  )
)

;; Admin: Update reward rate
(define-public (update-reward-rate (new-rate uint))
  (begin
    (asserts! (is-eq tx-sender contract-owner) err-owner-only)
    (var-set reward-rate-per-block new-rate)
    (ok new-rate)
  )
)

;; Admin: Update minimum stake
(define-public (update-min-stake (new-min uint))
  (begin
    (asserts! (is-eq tx-sender contract-owner) err-owner-only)
    (var-set min-stake-amount new-min)
    (ok new-min)
  )
)

;; Admin: Update cooldown period
(define-public (update-cooldown-period (new-period uint))
  (begin
    (asserts! (is-eq tx-sender contract-owner) err-owner-only)
    (var-set cooldown-period new-period)
    (ok new-period)
  )
)

;; Admin: Set TLX token contract (call once after deployment)
(define-public (set-tlx-token-contract (token-contract principal))
  (begin
    (asserts! (is-eq tx-sender contract-owner) err-owner-only)
    (var-set tlx-token-contract token-contract)
    (print { event: "tlx-token-set", contract: token-contract })
    (ok true)
  )
)

;; Read-only: Get TLX token contract
(define-read-only (get-tlx-token-contract)
  (var-get tlx-token-contract)
)

;; Private helper functions

;; Record staking history
(define-private (record-history (address principal) (action (string-ascii 16)) (amount uint) (rewards uint))
  (let (
    (current-count (default-to { count: u0 } (map-get? user-history-count { address: address })))
    (new-index (get count current-count))
  )
    (map-set staking-history
      { address: address, index: new-index }
      {
        action: action,
        amount: amount,
        block-height: stacks-block-height,
        rewards-claimed: rewards
      }
    )
    (map-set user-history-count
      { address: address }
      { count: (+ new-index u1) }
    )
    true
  )
)

Functions (23)

FunctionAccessArgs
get-reward-rateread-only
get-staker-inforead-onlyaddress: principal
get-tier-inforead-onlytier: uint
calculate-pending-rewardsread-onlyaddress: principal
get-total-stakedread-only
get-reward-poolread-only
calculate-apyread-onlytier: uint
determine-tierread-onlyblocks-locked: uint
is-in-cooldownread-onlyaddress: principal
get-staking-historyread-onlyaddress: principal, index: uint
stakepublicamount: uint, lock-duration: uint
add-stakepublicamount: uint
claim-rewardspublic
initiate-unstakepublic
complete-unstakepublic
cancel-unstakepublic
fund-reward-poolpublicamount: uint
update-reward-ratepublicnew-rate: uint
update-min-stakepublicnew-min: uint
update-cooldown-periodpublicnew-period: uint
set-tlx-token-contractpublictoken-contract: principal
get-tlx-token-contractread-only
record-historyprivateaddress: principal, action: (string-ascii 16