Source Code

;; Staking Pool Contract
;; Stake STX tokens and earn rewards
;; Built by rajuice for Stacks Builder Rewards

;; Constants
(define-constant CONTRACT-OWNER tx-sender)
(define-constant ERR-NOT-AUTHORIZED (err u401))
(define-constant ERR-INSUFFICIENT-BALANCE (err u402))
(define-constant ERR-INVALID-AMOUNT (err u403))
(define-constant ERR-NO-STAKE (err u404))
(define-constant ERR-STAKE-LOCKED (err u405))
(define-constant ERR-POOL-EMPTY (err u406))

;; Staking parameters
(define-constant MIN-STAKE u1000000) ;; Minimum 1 STX
(define-constant LOCK-PERIOD u144) ;; ~24 hours in blocks
(define-constant REWARD-RATE u100) ;; 1% rewards per period (100 basis points)

;; Data vars
(define-data-var total-staked uint u0)
(define-data-var reward-pool uint u0)
(define-data-var staking-enabled bool true)
(define-data-var total-stakers uint u0)

;; Data maps
(define-map stakers principal {
  amount: uint,
  start-block: uint,
  rewards-claimed: uint
})

;; Private function to get contract principal
(define-private (get-contract-principal)
  (as-contract tx-sender))

;; Staking Functions

;; Stake STX tokens
(define-public (stake (amount uint))
  (let (
    (current-stake (default-to { amount: u0, start-block: u0, rewards-claimed: u0 } (map-get? stakers tx-sender)))
    (new-amount (+ (get amount current-stake) amount))
    (contract-address (get-contract-principal))
  )
    (asserts! (var-get staking-enabled) ERR-NOT-AUTHORIZED)
    (asserts! (>= amount MIN-STAKE) ERR-INVALID-AMOUNT)
    ;; Transfer STX to contract
    (try! (stx-transfer? amount tx-sender contract-address))
    ;; Update staker info
    (if (is-eq (get amount current-stake) u0)
      (var-set total-stakers (+ (var-get total-stakers) u1))
      true
    )
    (map-set stakers tx-sender {
      amount: new-amount,
      start-block: stacks-block-height,
      rewards-claimed: (get rewards-claimed current-stake)
    })
    ;; Update total staked
    (var-set total-staked (+ (var-get total-staked) amount))
    (ok new-amount)))

;; Private function for withdrawing funds
(define-private (withdraw-stx (amount uint) (recipient principal))
  (as-contract (stx-transfer? amount tx-sender recipient)))

;; Unstake tokens
(define-public (unstake (amount uint))
  (let (
    (staker-info (unwrap! (map-get? stakers tx-sender) ERR-NO-STAKE))
    (staked-amount (get amount staker-info))
    (start-block (get start-block staker-info))
    (caller tx-sender)
  )
    (asserts! (>= staked-amount amount) ERR-INSUFFICIENT-BALANCE)
    (asserts! (>= stacks-block-height (+ start-block LOCK-PERIOD)) ERR-STAKE-LOCKED)
    ;; Transfer STX back to user
    (try! (withdraw-stx amount caller))
    ;; Update staker info
    (if (is-eq staked-amount amount)
      (begin
        (map-delete stakers caller)
        (var-set total-stakers (- (var-get total-stakers) u1)))
      (map-set stakers caller {
        amount: (- staked-amount amount),
        start-block: start-block,
        rewards-claimed: (get rewards-claimed staker-info)
      }))
    ;; Update total staked
    (var-set total-staked (- (var-get total-staked) amount))
    (ok amount)))

;; Claim rewards
(define-public (claim-rewards)
  (let (
    (staker-info (unwrap! (map-get? stakers tx-sender) ERR-NO-STAKE))
    (pending-rewards (calculate-pending-rewards tx-sender))
    (caller tx-sender)
  )
    (asserts! (> pending-rewards u0) ERR-INSUFFICIENT-BALANCE)
    (asserts! (>= (var-get reward-pool) pending-rewards) ERR-POOL-EMPTY)
    ;; Transfer rewards
    (try! (withdraw-stx pending-rewards caller))
    ;; Update claimed rewards
    (map-set stakers caller {
      amount: (get amount staker-info),
      start-block: (get start-block staker-info),
      rewards-claimed: (+ (get rewards-claimed staker-info) pending-rewards)
    })
    ;; Update reward pool
    (var-set reward-pool (- (var-get reward-pool) pending-rewards))
    (ok pending-rewards)))

;; Compound rewards (claim and restake)
(define-public (compound)
  (let (
    (staker-info (unwrap! (map-get? stakers tx-sender) ERR-NO-STAKE))
    (pending-rewards (calculate-pending-rewards tx-sender))
    (new-amount (+ (get amount staker-info) pending-rewards))
  )
    (asserts! (> pending-rewards u0) ERR-INSUFFICIENT-BALANCE)
    (asserts! (>= (var-get reward-pool) pending-rewards) ERR-POOL-EMPTY)
    ;; Update staker with compounded amount
    (map-set stakers tx-sender {
      amount: new-amount,
      start-block: stacks-block-height,
      rewards-claimed: (+ (get rewards-claimed staker-info) pending-rewards)
    })
    ;; Update totals
    (var-set total-staked (+ (var-get total-staked) pending-rewards))
    (var-set reward-pool (- (var-get reward-pool) pending-rewards))
    (ok new-amount)))

;; Helper function to calculate pending rewards
(define-read-only (calculate-pending-rewards (staker principal))
  (let (
    (staker-info (default-to { amount: u0, start-block: u0, rewards-claimed: u0 } (map-get? stakers staker)))
    (staked-amount (get amount staker-info))
    (start-block (get start-block staker-info))
    (blocks-staked (- stacks-block-height start-block))
    (periods-completed (/ blocks-staked LOCK-PERIOD))
  )
    (if (> staked-amount u0)
      (/ (* staked-amount (* REWARD-RATE periods-completed)) u10000)
      u0)))

;; Admin Functions

;; Add to reward pool
(define-public (add-rewards (amount uint))
  (let ((contract-address (get-contract-principal)))
    (try! (stx-transfer? amount tx-sender contract-address))
    (var-set reward-pool (+ (var-get reward-pool) amount))
    (ok (var-get reward-pool))))

;; Toggle staking (owner only)
(define-public (toggle-staking)
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (var-set staking-enabled (not (var-get staking-enabled)))
    (ok (var-get staking-enabled))))

;; Emergency withdraw (owner only)
(define-public (emergency-withdraw)
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (let ((balance (stx-get-balance (get-contract-principal))))
      (try! (withdraw-stx balance CONTRACT-OWNER))
      (var-set total-staked u0)
      (var-set reward-pool u0)
      (ok balance))))

;; Read-only Functions

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

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

(define-read-only (get-reward-pool)
  (ok (var-get reward-pool)))

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

(define-read-only (is-staking-enabled)
  (ok (var-get staking-enabled)))

(define-read-only (get-min-stake)
  (ok MIN-STAKE))

(define-read-only (get-lock-period)
  (ok LOCK-PERIOD))

(define-read-only (get-reward-rate)
  (ok REWARD-RATE))

(define-read-only (get-unlock-block (staker principal))
  (let ((staker-info (map-get? stakers staker)))
    (match staker-info
      info (ok (+ (get start-block info) LOCK-PERIOD))
      (err ERR-NO-STAKE))))

Functions (19)

FunctionAccessArgs
get-total-stakedread-only
get-contract-principalprivate
stakepublicamount: uint
withdraw-stxprivateamount: uint, recipient: principal
unstakepublicamount: uint
claim-rewardspublic
compoundpublic
calculate-pending-rewardsread-onlystaker: principal
add-rewardspublicamount: uint
toggle-stakingpublic
emergency-withdrawpublic
get-staker-inforead-onlystaker: principal
get-reward-poolread-only
get-total-stakersread-only
is-staking-enabledread-only
get-min-stakeread-only
get-lock-periodread-only
get-reward-rateread-only
get-unlock-blockread-onlystaker: principal