Source Code

;; =============================================================================
;; B2S Rewards Distributor
;; Automatic reward distribution for B2S stakers
;; =============================================================================

(define-constant contract-owner tx-sender)

;; -----------------------------------------------------------------------------
;; Error codes
;; -----------------------------------------------------------------------------
(define-constant err-not-authorized       (err u401))
(define-constant err-insufficient-balance (err u402))
(define-constant err-not-staked           (err u403))
(define-constant err-too-soon             (err u404))
(define-constant err-invalid-amount       (err u405))
(define-constant err-below-minimum        (err u406))
(define-constant err-above-maximum        (err u407))

;; -----------------------------------------------------------------------------
;; Constants
;; blocks-per-day  = 144  (~10 min blocks)
;; blocks-per-year = 144 * 365 = 52560
;; precision       = 1_000_000 (6 decimals)
;; min-stake       = 1 token  (1_000_000 micro-units)
;; max-stake       = 1M tokens
;; -----------------------------------------------------------------------------
(define-constant blocks-per-day  u144)
(define-constant blocks-per-year u52560)
(define-constant precision        u1000000)
(define-constant min-stake        u1000000)
(define-constant max-stake        u1000000000000)

;; -----------------------------------------------------------------------------
;; State variables
;; base-apy: 12.5% = 125000 / 1_000_000
;; -----------------------------------------------------------------------------
(define-data-var base-apy                  uint u125000)
(define-data-var total-staked              uint u0)
(define-data-var total-rewards-distributed uint u0)

;; -----------------------------------------------------------------------------
;; Maps
;; -----------------------------------------------------------------------------
(define-map staker-info principal {
  staked-amount:        uint,
  stake-block:          uint,
  last-reward-block:    uint,
  total-rewards-earned: uint
})

;; =============================================================================
;; Private helpers
;; =============================================================================

;; reward = staked * apy * blocks-elapsed / (precision * blocks-per-year)
(define-private (compute-rewards (staked uint) (last-reward-block uint))
  (let (
    (blocks-elapsed (- block-height last-reward-block))
    (apy            (var-get base-apy))
  )
    (/ (* (* staked apy) blocks-elapsed)
       (* precision blocks-per-year))
  )
)

;; Internal claim: updates state and returns reward amount (0 if nothing to claim)
(define-private (do-claim (staker principal))
  (match (map-get? staker-info staker)
    info
      (let (
        (staked  (get staked-amount info))
        (pending (compute-rewards staked (get last-reward-block info)))
      )
        (if (> pending u0)
          (begin
            (map-set staker-info staker (merge info {
              last-reward-block:    block-height,
              total-rewards-earned: (+ (get total-rewards-earned info) pending)
            }))
            (var-set total-rewards-distributed
              (+ (var-get total-rewards-distributed) pending))
            pending
          )
          u0
        )
      )
    u0
  )
)

;; =============================================================================
;; Public functions
;; =============================================================================

;; -----------------------------------------------------------------------------
;; Stake B2S tokens
;; -----------------------------------------------------------------------------
(define-public (stake (amount uint))
  (let (
    (staker       tx-sender)
    (current-info (default-to
      { staked-amount: u0, stake-block: u0, last-reward-block: u0, total-rewards-earned: u0 }
      (map-get? staker-info staker)))
    (new-total    (+ (get staked-amount current-info) amount))
  )
    (asserts! (> amount u0)            err-invalid-amount)
    (asserts! (>= amount min-stake)    err-below-minimum)
    (asserts! (<= new-total max-stake) err-above-maximum)

    (let (
      (claimed (do-claim staker))
    )
      ;; TODO: add ft-transfer? here once B2S token contract is known
      ;; (try! (contract-call? .b2s-token transfer amount staker (as-contract tx-sender) none))

      (map-set staker-info staker (merge current-info {
        staked-amount:     new-total,
        stake-block:       block-height,
        last-reward-block: block-height
      }))

      (var-set total-staked (+ (var-get total-staked) amount))
      (ok amount)
    )
  )
)

;; -----------------------------------------------------------------------------
;; Claim pending rewards
;; -----------------------------------------------------------------------------
(define-public (claim-rewards)
  (let (
    (staker  tx-sender)
    (pending (do-claim staker))
  )
    (asserts! (is-some (map-get? staker-info staker)) err-not-staked)
    (asserts! (> pending u0)                          err-too-soon)

    ;; TODO: transfer reward tokens to staker
    ;; (try! (as-contract (contract-call? .b2s-token transfer pending tx-sender staker none)))

    (ok pending)
  )
)

;; -----------------------------------------------------------------------------
;; Unstake tokens (claims pending rewards first)
;; -----------------------------------------------------------------------------
(define-public (unstake (amount uint))
  (let (
    (staker tx-sender)
    (info   (unwrap! (map-get? staker-info staker) err-not-staked))
    (staked (get staked-amount info))
  )
    (asserts! (> amount u0)      err-invalid-amount)
    (asserts! (>= staked amount) err-insufficient-balance)

    (let (
      (claimed (do-claim staker))
    )
      ;; TODO: return B2S tokens to staker
      ;; (try! (as-contract (contract-call? .b2s-token transfer amount tx-sender staker none)))

      (map-set staker-info staker (merge info {
        staked-amount: (- staked amount)
      }))

      (var-set total-staked (- (var-get total-staked) amount))
      (ok amount)
    )
  )
)

;; -----------------------------------------------------------------------------
;; Admin: update base APY (owner only)
;; e.g. u125000 = 12.5%,  u200000 = 20%
;; -----------------------------------------------------------------------------
(define-public (set-base-apy (new-apy uint))
  (begin
    (asserts! (is-eq tx-sender contract-owner) err-not-authorized)
    (asserts! (> new-apy u0)                   err-invalid-amount)
    (var-set base-apy new-apy)
    (ok new-apy)
  )
)

;; =============================================================================
;; Read-only functions
;; =============================================================================

(define-read-only (get-pending-rewards (staker principal))
  (match (map-get? staker-info staker)
    info (ok (compute-rewards (get staked-amount info) (get last-reward-block info)))
    (ok u0)
  )
)

(define-read-only (get-staker-info (staker principal))
  (ok (map-get? staker-info staker))
)

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

(define-read-only (get-total-rewards-distributed)
  (ok (var-get total-rewards-distributed))
)

(define-read-only (get-base-apy)
  (ok (var-get base-apy))
)

;; Estimate rewards for a given stake amount over N days
(define-read-only (estimate-rewards (amount uint) (days uint))
  (ok (/ (* (* amount (var-get base-apy)) (* days blocks-per-day))
         (* precision blocks-per-year)))
)

Functions (12)

FunctionAccessArgs
compute-rewardsprivatestaked: uint, last-reward-block: uint
do-claimprivatestaker: principal
stakepublicamount: uint
claim-rewardspublic
unstakepublicamount: uint
set-base-apypublicnew-apy: uint
get-pending-rewardsread-onlystaker: principal
get-staker-inforead-onlystaker: principal
get-total-stakedread-only
get-total-rewards-distributedread-only
get-base-apyread-only
estimate-rewardsread-onlyamount: uint, days: uint