Source Code

;; title: vault-staking
;; version: 1.0.0
;; summary: Governance token staking mechanism
;; description: Stake tokens for governance power and rewards - Clarity 4

;; Constants
(define-constant CONTRACT-OWNER tx-sender)
(define-constant ERR-UNAUTHORIZED (err u800))
(define-constant ERR-INVALID-AMOUNT (err u801))
(define-constant ERR-NO-STAKE (err u802))
(define-constant ERR-LOCKED (err u803))
(define-constant ERR-INVALID-LOCK-PERIOD (err u804))
(define-constant ERR-ALREADY-LOCKED (err u805))

;; Lock periods (in seconds)
(define-constant LOCK-PERIOD-NONE u0)
(define-constant LOCK-PERIOD-1-MONTH u2592000)   ;; 30 days
(define-constant LOCK-PERIOD-3-MONTHS u7776000)  ;; 90 days
(define-constant LOCK-PERIOD-6-MONTHS u15552000) ;; 180 days
(define-constant LOCK-PERIOD-1-YEAR u31536000)   ;; 365 days

;; Voting power multipliers (in basis points)
(define-constant MULTIPLIER-NONE u10000)      ;; 1x (100%)
(define-constant MULTIPLIER-1-MONTH u12000)   ;; 1.2x (120%)
(define-constant MULTIPLIER-3-MONTHS u15000)  ;; 1.5x (150%)
(define-constant MULTIPLIER-6-MONTHS u20000)  ;; 2x (200%)
(define-constant MULTIPLIER-1-YEAR u30000)    ;; 3x (300%)

;; Early withdrawal penalty (basis points)
(define-constant EARLY-WITHDRAWAL-PENALTY u2000)  ;; 20%

;; Data Variables
(define-data-var total-staked uint u0)
(define-data-var total-voting-power uint u0)
(define-data-var penalty-pool uint u0)
(define-data-var staking-paused bool false)

;; Data Maps - Using stacks-block-time for Clarity 4
(define-map stakes principal {
  amount-staked: uint,
  voting-power: uint,
  lock-period: uint,
  locked-until: uint,     ;; Clarity 4: Unix timestamp
  staked-at: uint,        ;; Clarity 4: Unix timestamp
  last-claim-time: uint,  ;; Clarity 4: Unix timestamp
  multiplier: uint
})

(define-map stake-history uint {
  user: principal,
  action: (string-ascii 20),  ;; "stake", "unstake", "lock"
  amount: uint,
  timestamp: uint  ;; Clarity 4: Unix timestamp
})

(define-data-var next-history-id uint u1)

;; Staking tiers
(define-map staking-tiers uint {
  min-amount: uint,
  tier-name: (string-ascii 20),
  benefits: (string-ascii 100)
})

;; Initialize staking tiers
(map-set staking-tiers u1 {
  min-amount: u1000000,      ;; 1 STX
  tier-name: "Bronze",
  benefits: "Basic voting rights"
})

(map-set staking-tiers u2 {
  min-amount: u10000000,     ;; 10 STX
  tier-name: "Silver",
  benefits: "Enhanced voting + Priority support"
})

(map-set staking-tiers u3 {
  min-amount: u50000000,     ;; 50 STX
  tier-name: "Gold",
  benefits: "2x voting + Fee discounts"
})

(map-set staking-tiers u4 {
  min-amount: u100000000,    ;; 100 STX
  tier-name: "Platinum",
  benefits: "3x voting + Revenue share"
})

;; Private Functions

(define-private (calculate-voting-power (amount uint) (multiplier uint))
  (/ (* amount multiplier) u10000)
)

(define-private (get-multiplier-for-lock-period (lock-period uint))
  (if (is-eq lock-period LOCK-PERIOD-1-YEAR)
    MULTIPLIER-1-YEAR
    (if (is-eq lock-period LOCK-PERIOD-6-MONTHS)
      MULTIPLIER-6-MONTHS
      (if (is-eq lock-period LOCK-PERIOD-3-MONTHS)
        MULTIPLIER-3-MONTHS
        (if (is-eq lock-period LOCK-PERIOD-1-MONTH)
          MULTIPLIER-1-MONTH
          MULTIPLIER-NONE
        )
      )
    )
  )
)

;; Public Functions

;; Stake tokens
(define-public (stake (amount uint) (lock-period uint))
  (let (
    (current-stake (default-to
      { amount-staked: u0, voting-power: u0, lock-period: u0, locked-until: u0,
        staked-at: u0, last-claim-time: u0, multiplier: MULTIPLIER-NONE }
      (map-get? stakes tx-sender)))
    (multiplier (get-multiplier-for-lock-period lock-period))
    (new-amount (+ (get amount-staked current-stake) amount))
    (new-voting-power (calculate-voting-power new-amount multiplier))
    (locked-until (if (> lock-period u0)
                    (+ stacks-block-time lock-period)
                    u0))
    (history-id (var-get next-history-id))
  )
    (asserts! (not (var-get staking-paused)) ERR-UNAUTHORIZED)
    (asserts! (> amount u0) ERR-INVALID-AMOUNT)
    (asserts! (or
      (is-eq lock-period LOCK-PERIOD-NONE)
      (is-eq lock-period LOCK-PERIOD-1-MONTH)
      (is-eq lock-period LOCK-PERIOD-3-MONTHS)
      (is-eq lock-period LOCK-PERIOD-6-MONTHS)
      (is-eq lock-period LOCK-PERIOD-1-YEAR)
    ) ERR-INVALID-LOCK-PERIOD)

    ;; Transfer tokens to contract
    (try! (stx-transfer? amount tx-sender tx-sender))

    ;; Update stake
    (map-set stakes tx-sender {
      amount-staked: new-amount,
      voting-power: new-voting-power,
      lock-period: lock-period,
      locked-until: locked-until,
      staked-at: stacks-block-time,
      last-claim-time: stacks-block-time,
      multiplier: multiplier
    })

    ;; Update globals
    (var-set total-staked (+ (var-get total-staked) amount))
    (var-set total-voting-power (+ (var-get total-voting-power) new-voting-power))

    ;; Record history
    (map-set stake-history history-id {
      user: tx-sender,
      action: "stake",
      amount: amount,
      timestamp: stacks-block-time
    })
    (var-set next-history-id (+ history-id u1))

    (print {
      event: "tokens-staked",
      user: tx-sender,
      amount: amount,
      lock-period: lock-period,
      voting-power: new-voting-power,
      timestamp: stacks-block-time
    })

    (ok new-voting-power)
  )
)

;; Unstake tokens
(define-public (unstake (amount uint))
  (let (
    (user-stake (unwrap! (map-get? stakes tx-sender) ERR-NO-STAKE))
    (locked-until (get locked-until user-stake))
    (is-locked (> locked-until stacks-block-time))
    (penalty-amount (if is-locked
                      (/ (* amount EARLY-WITHDRAWAL-PENALTY) u10000)
                      u0))
    (withdraw-amount (- amount penalty-amount))
    (remaining-amount (- (get amount-staked user-stake) amount))
    (new-voting-power (calculate-voting-power remaining-amount (get multiplier user-stake)))
    (history-id (var-get next-history-id))
  )
    (asserts! (>= (get amount-staked user-stake) amount) ERR-INVALID-AMOUNT)
    (asserts! (> amount u0) ERR-INVALID-AMOUNT)

    ;; Update stake
    (if (is-eq remaining-amount u0)
      (map-delete stakes tx-sender)
      (map-set stakes tx-sender
        (merge user-stake {
          amount-staked: remaining-amount,
          voting-power: new-voting-power
        }))
    )

    ;; Transfer tokens back (minus penalty if locked)
    (try! (begin (stx-transfer? withdraw-amount tx-sender tx-sender)))

    ;; Add penalty to pool if applicable
    (if (> penalty-amount u0)
      (var-set penalty-pool (+ (var-get penalty-pool) penalty-amount))
      true
    )

    ;; Update globals
    (var-set total-staked (- (var-get total-staked) amount))
    (var-set total-voting-power (- (var-get total-voting-power) (get voting-power user-stake)))

    ;; Record history
    (map-set stake-history history-id {
      user: tx-sender,
      action: "unstake",
      amount: amount,
      timestamp: stacks-block-time
    })
    (var-set next-history-id (+ history-id u1))

    (print {
      event: "tokens-unstaked",
      user: tx-sender,
      amount: amount,
      penalty: penalty-amount,
      actual-withdrawn: withdraw-amount,
      timestamp: stacks-block-time
    })

    (ok withdraw-amount)
  )
)

;; Extend lock period
(define-public (extend-lock (new-lock-period uint))
  (let (
    (user-stake (unwrap! (map-get? stakes tx-sender) ERR-NO-STAKE))
    (current-lock (get lock-period user-stake))
    (new-multiplier (get-multiplier-for-lock-period new-lock-period))
    (new-locked-until (+ stacks-block-time new-lock-period))
    (new-voting-power (calculate-voting-power (get amount-staked user-stake) new-multiplier))
    (history-id (var-get next-history-id))
  )
    (asserts! (> new-lock-period current-lock) ERR-INVALID-LOCK-PERIOD)
    (asserts! (or
      (is-eq new-lock-period LOCK-PERIOD-1-MONTH)
      (is-eq new-lock-period LOCK-PERIOD-3-MONTHS)
      (is-eq new-lock-period LOCK-PERIOD-6-MONTHS)
      (is-eq new-lock-period LOCK-PERIOD-1-YEAR)
    ) ERR-INVALID-LOCK-PERIOD)

    ;; Update stake with new lock
    (map-set stakes tx-sender
      (merge user-stake {
        lock-period: new-lock-period,
        locked-until: new-locked-until,
        multiplier: new-multiplier,
        voting-power: new-voting-power
      }))

    ;; Update total voting power
    (var-set total-voting-power
      (+ (- (var-get total-voting-power) (get voting-power user-stake)) new-voting-power))

    ;; Record history
    (map-set stake-history history-id {
      user: tx-sender,
      action: "lock-extended",
      amount: (get amount-staked user-stake),
      timestamp: stacks-block-time
    })
    (var-set next-history-id (+ history-id u1))

    (print {
      event: "lock-extended",
      user: tx-sender,
      new-lock-period: new-lock-period,
      new-voting-power: new-voting-power,
      timestamp: stacks-block-time
    })

    (ok new-voting-power)
  )
)

;; Distribute penalty pool (admin only)
(define-public (distribute-penalties (recipients (list 10 principal)) (amounts (list 10 uint)))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-UNAUTHORIZED)

    ;; This would distribute penalties to recipients
    ;; Simplified implementation for now

    (print {
      event: "penalties-distributed",
      timestamp: stacks-block-time
    })

    (ok true)
  )
)

;; Pause staking (admin only)
(define-public (pause-staking)
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-UNAUTHORIZED)
    (var-set staking-paused true)
    (ok true)
  )
)

;; Resume staking (admin only)
(define-public (resume-staking)
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-UNAUTHORIZED)
    (var-set staking-paused false)
    (ok true)
  )
)

;; Read-Only Functions

(define-read-only (get-stake (user principal))
  (map-get? stakes user)
)

(define-read-only (get-voting-power (user principal))
  (match (map-get? stakes user)
    stake-info (ok (get voting-power stake-info))
    (ok u0)
  )
)

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

(define-read-only (get-total-voting-power)
  (var-get total-voting-power)
)

(define-read-only (get-penalty-pool)
  (var-get penalty-pool)
)

(define-read-only (is-locked (user principal))
  (match (map-get? stakes user)
    stake-data (> (get locked-until stake-data) stacks-block-time)
    false
  )
)

(define-read-only (get-stake-history (history-id uint))
  (map-get? stake-history history-id)
)

(define-read-only (get-staking-tier (tier-id uint))
  (map-get? staking-tiers tier-id)
)

(define-read-only (is-staking-paused)
  (var-get staking-paused)
)

;; Clarity 4 Enhanced Functions

;; 1. Clarity 4: principal-destruct? - Validate staker principals
(define-read-only (validate-staker (staker principal))
  (principal-destruct? staker)
)

;; 2. Clarity 4: int-to-utf8 - Format staking amounts
(define-read-only (format-stake-amount (user principal))
  (match (get-stake user)
    stake-data (ok (int-to-utf8 (get amount-staked stake-data)))
    (ok (int-to-utf8 u0))
  )
)

;; 3. Clarity 4: string-to-uint? - Parse lock periods
(define-read-only (parse-lock-period (period-str (string-ascii 20)))
  (match (string-to-uint? period-str)
    period (ok period)
    (err u998)
  )
)

;; 4. Clarity 4: burn-block-height - Track stake timing
(define-read-only (get-staking-timestamps)
  (ok {
    stacks-time: stacks-block-time,
    burn-time: burn-block-height
  })
)

Functions (21)

FunctionAccessArgs
calculate-voting-powerprivateamount: uint, multiplier: uint
get-multiplier-for-lock-periodprivatelock-period: uint
stakepublicamount: uint, lock-period: uint
unstakepublicamount: uint
extend-lockpublicnew-lock-period: uint
distribute-penaltiespublicrecipients: (list 10 principal
pause-stakingpublic
resume-stakingpublic
get-stakeread-onlyuser: principal
get-voting-powerread-onlyuser: principal
get-total-stakedread-only
get-total-voting-powerread-only
get-penalty-poolread-only
is-lockedread-onlyuser: principal
get-stake-historyread-onlyhistory-id: uint
get-staking-tierread-onlytier-id: uint
is-staking-pausedread-only
validate-stakerread-onlystaker: principal
format-stake-amountread-onlyuser: principal
parse-lock-periodread-onlyperiod-str: (string-ascii 20
get-staking-timestampsread-only