Source Code

;; Sentinel Staking Contract
;; Advanced staking with referral rewards and tiered bonuses

(define-constant CONTRACT_OWNER tx-sender)
(define-constant ERR_NOT_AUTHORIZED (err u100))
(define-constant ERR_INSUFFICIENT_BALANCE (err u101))
(define-constant ERR_NO_STAKE (err u102))
(define-constant ERR_STAKE_LOCKED (err u103))
(define-constant ERR_INVALID_REFERRER (err u104))
(define-constant ERR_SELF_REFERRAL (err u105))
(define-constant ERR_ALREADY_STAKING (err u106))

;; Token reference
(define-constant SENTINEL_TOKEN 'SP2PEBKJ2W1ZDDF2QQ6Y4FXKZEDPT9J9R2NKD9WJB.sentinel-token)

;; Staking tiers (in SNTL tokens - 6 decimals)
(define-constant TIER_BRONZE u0)           ;; 0+ SNTL
(define-constant TIER_SILVER u10000000000) ;; 10,000 SNTL
(define-constant TIER_GOLD u50000000000)   ;; 50,000 SNTL
(define-constant TIER_PLATINUM u100000000000) ;; 100,000 SNTL

;; APY rates (basis points - 10000 = 100%)
(define-constant APY_BRONZE u800)    ;; 8% APY
(define-constant APY_SILVER u1000)   ;; 10% APY
(define-constant APY_GOLD u1200)     ;; 12% APY
(define-constant APY_PLATINUM u1500) ;; 15% APY

;; Referral rewards (basis points)
(define-constant REFERRAL_REWARD_RATE u500) ;; 5% of staking rewards go to referrer

;; Lock periods (in blocks, ~10 min per block)
(define-constant MIN_LOCK_PERIOD u4320)   ;; ~30 days
(define-constant BONUS_LOCK_PERIOD u12960) ;; ~90 days (25% bonus)
(define-constant MAX_LOCK_PERIOD u25920)  ;; ~180 days (50% bonus)

;; Data structures
(define-map stakes 
  { staker: principal }
  {
    amount: uint,
    start-block: uint,
    lock-period: uint,
    last-claim-block: uint,
    referrer: (optional principal),
    tier: uint
  }
)

(define-map referral-earnings
  { referrer: principal }
  {
    total-earned: uint,
    referral-count: uint,
    pending: uint
  }
)

(define-map total-staked-by-tier
  { tier: uint }
  { amount: uint }
)

;; Data variables
(define-data-var total-staked uint u0)
(define-data-var total-stakers uint u0)
(define-data-var total-rewards-distributed uint u0)
(define-data-var contract-paused bool false)

;; Read-only functions

(define-read-only (get-stake (staker principal))
  (map-get? stakes { staker: staker })
)

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

(define-read-only (get-total-stakers)
  (var-get total-stakers)
)

(define-read-only (get-referral-stats (referrer principal))
  (default-to 
    { total-earned: u0, referral-count: u0, pending: u0 }
    (map-get? referral-earnings { referrer: referrer })
  )
)

(define-read-only (calculate-tier (amount uint))
  (if (>= amount TIER_PLATINUM)
    u3  ;; Platinum
    (if (>= amount TIER_GOLD)
      u2  ;; Gold
      (if (>= amount TIER_SILVER)
        u1  ;; Silver
        u0  ;; Bronze
      )
    )
  )
)

(define-read-only (get-tier-apy (tier uint))
  (if (is-eq tier u3)
    APY_PLATINUM
    (if (is-eq tier u2)
      APY_GOLD
      (if (is-eq tier u1)
        APY_SILVER
        APY_BRONZE
      )
    )
  )
)

(define-read-only (get-lock-bonus (lock-period uint))
  (if (>= lock-period MAX_LOCK_PERIOD)
    u5000  ;; 50% bonus
    (if (>= lock-period BONUS_LOCK_PERIOD)
      u2500  ;; 25% bonus
      u0     ;; No bonus
    )
  )
)

(define-read-only (calculate-pending-rewards (staker principal))
  (match (get-stake staker)
    stake-info
    (let (
      (blocks-staked (- stacks-block-height (get last-claim-block stake-info)))
      (base-apy (get-tier-apy (get tier stake-info)))
      (lock-bonus (get-lock-bonus (get lock-period stake-info)))
      (effective-apy (+ base-apy (/ (* base-apy lock-bonus) u10000)))
      (yearly-reward (/ (* (get amount stake-info) effective-apy) u10000))
      ;; Approximate blocks per year: 52,560 (assuming 10 min blocks)
      (reward (/ (* yearly-reward blocks-staked) u52560))
    )
    reward
    )
    u0
  )
)

(define-read-only (is-stake-unlocked (staker principal))
  (match (get-stake staker)
    stake-info
    (>= stacks-block-height (+ (get start-block stake-info) (get lock-period stake-info)))
    false
  )
)

;; Public functions

(define-public (stake (amount uint) (lock-period uint) (referrer (optional principal)))
  (let (
    (staker tx-sender)
    (tier (calculate-tier amount))
  )
    ;; Validations
    (asserts! (not (var-get contract-paused)) ERR_NOT_AUTHORIZED)
    (asserts! (> amount u0) ERR_INSUFFICIENT_BALANCE)
    (asserts! (>= lock-period MIN_LOCK_PERIOD) ERR_STAKE_LOCKED)
    (asserts! (is-none (get-stake staker)) ERR_ALREADY_STAKING)
    
    ;; Validate referrer
    (match referrer
      ref
      (begin
        (asserts! (not (is-eq ref staker)) ERR_SELF_REFERRAL)
        ;; Referrer must have staked before
        (asserts! (is-some (get-stake ref)) ERR_INVALID_REFERRER)
      )
      true
    )
    
    ;; Transfer tokens to contract
    ;; (try! (contract-call? SENTINEL_TOKEN transfer amount staker (as-contract tx-sender) none))
    
    ;; Create stake
    (map-set stakes
      { staker: staker }
      {
        amount: amount,
        start-block: stacks-block-height,
        lock-period: lock-period,
        last-claim-block: stacks-block-height,
        referrer: referrer,
        tier: tier
      }
    )
    
    ;; Update referral stats
    (match referrer
      ref
      (let (
        (current-stats (get-referral-stats ref))
      )
        (map-set referral-earnings
          { referrer: ref }
          {
            total-earned: (get total-earned current-stats),
            referral-count: (+ (get referral-count current-stats) u1),
            pending: (get pending current-stats)
          }
        )
      )
      true
    )
    
    ;; Update totals
    (var-set total-staked (+ (var-get total-staked) amount))
    (var-set total-stakers (+ (var-get total-stakers) u1))
    
    (ok { 
      amount: amount, 
      tier: tier, 
      lock-period: lock-period,
      apy: (get-tier-apy tier)
    })
  )
)

(define-public (claim-rewards)
  (let (
    (staker tx-sender)
    (stake-data (unwrap! (get-stake staker) ERR_NO_STAKE))
    (rewards (calculate-pending-rewards staker))
    (referrer-reward (/ (* rewards REFERRAL_REWARD_RATE) u10000))
  )
    (asserts! (> rewards u0) ERR_INSUFFICIENT_BALANCE)
    
    ;; Update last claim block
    (map-set stakes
      { staker: staker }
      (merge stake-data { last-claim-block: stacks-block-height })
    )
    
    ;; Process referrer reward
    (match (get referrer stake-data)
      ref
      (let (
        (ref-stats (get-referral-stats ref))
      )
        (map-set referral-earnings
          { referrer: ref }
          {
            total-earned: (get total-earned ref-stats),
            referral-count: (get referral-count ref-stats),
            pending: (+ (get pending ref-stats) referrer-reward)
          }
        )
      )
      true
    )
    
    ;; Transfer rewards to staker
    ;; (try! (as-contract (contract-call? SENTINEL_TOKEN transfer (- rewards referrer-reward) (as-contract tx-sender) staker none)))
    
    (var-set total-rewards-distributed (+ (var-get total-rewards-distributed) rewards))
    
    (ok { rewards-claimed: rewards, referrer-share: referrer-reward })
  )
)

(define-public (unstake)
  (let (
    (staker tx-sender)
    (stake-data (unwrap! (get-stake staker) ERR_NO_STAKE))
  )
    ;; Check lock period
    (asserts! (is-stake-unlocked staker) ERR_STAKE_LOCKED)
    
    ;; Claim remaining rewards first
    (try! (claim-rewards))
    
    ;; Transfer staked tokens back
    ;; (try! (as-contract (contract-call? SENTINEL_TOKEN transfer (get amount stake-data) (as-contract tx-sender) staker none)))
    
    ;; Remove stake
    (map-delete stakes { staker: staker })
    
    ;; Update totals
    (var-set total-staked (- (var-get total-staked) (get amount stake-data)))
    (var-set total-stakers (- (var-get total-stakers) u1))
    
    (ok { amount-returned: (get amount stake-data) })
  )
)

(define-public (claim-referral-rewards)
  (let (
    (referrer tx-sender)
    (stats (get-referral-stats referrer))
    (pending (get pending stats))
  )
    (asserts! (> pending u0) ERR_INSUFFICIENT_BALANCE)
    
    ;; Update stats
    (map-set referral-earnings
      { referrer: referrer }
      {
        total-earned: (+ (get total-earned stats) pending),
        referral-count: (get referral-count stats),
        pending: u0
      }
    )
    
    ;; Transfer rewards
    ;; (try! (as-contract (contract-call? SENTINEL_TOKEN transfer pending (as-contract tx-sender) referrer none)))
    
    (ok { claimed: pending })
  )
)

(define-public (add-to-stake (additional-amount uint))
  (let (
    (staker tx-sender)
    (stake-data (unwrap! (get-stake staker) ERR_NO_STAKE))
    (new-amount (+ (get amount stake-data) additional-amount))
    (new-tier (calculate-tier new-amount))
  )
    (asserts! (> additional-amount u0) ERR_INSUFFICIENT_BALANCE)
    
    ;; Claim existing rewards first
    (try! (claim-rewards))
    
    ;; Transfer additional tokens
    ;; (try! (contract-call? SENTINEL_TOKEN transfer additional-amount staker (as-contract tx-sender) none))
    
    ;; Update stake
    (map-set stakes
      { staker: staker }
      (merge stake-data { 
        amount: new-amount,
        tier: new-tier
      })
    )
    
    ;; Update total
    (var-set total-staked (+ (var-get total-staked) additional-amount))
    
    (ok { new-total: new-amount, new-tier: new-tier })
  )
)

;; Admin functions

(define-public (pause-contract)
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED)
    (var-set contract-paused true)
    (ok true)
  )
)

(define-public (resume-contract)
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED)
    (var-set contract-paused false)
    (ok true)
  )
)

Functions (16)

FunctionAccessArgs
get-stakeread-onlystaker: principal
get-total-stakedread-only
get-total-stakersread-only
get-referral-statsread-onlyreferrer: principal
calculate-tierread-onlyamount: uint
get-tier-apyread-onlytier: uint
get-lock-bonusread-onlylock-period: uint
calculate-pending-rewardsread-onlystaker: principal
is-stake-unlockedread-onlystaker: principal
stakepublicamount: uint, lock-period: uint, referrer: (optional principal
claim-rewardspublic
unstakepublic
claim-referral-rewardspublic
add-to-stakepublicadditional-amount: uint
pause-contractpublic
resume-contractpublic