Source Code

;; @clarity-version 3
;; ============================================
;; Stacks Boost - Lending Pool (sBTC Deposit)
;; ============================================

;; ============================================
;; Error Constants
;; ============================================
(define-constant ERR_INVALID_WITHDRAW_AMOUNT (err u100))
(define-constant ERR_EXCEEDED_MAX_BORROW (err u101))
(define-constant ERR_CANNOT_BE_LIQUIDATED (err u102))
(define-constant ERR_INVALID_ORACLE (err u104))
(define-constant ERR_INVALID_SBTC_CONTRACT (err u105))
(define-constant ERR_INVALID_DEX_CONTRACT (err u106))
(define-constant ERR_UNAUTHORIZED (err u107))
(define-constant ERR_PAUSED (err u108))
(define-constant ERR_INVALID_BORROW_AMOUNT (err u109))
(define-constant ERR_INVALID_LIQUIDATION (err u110))
(define-constant ERR_INVALID_DEPOSIT_AMOUNT (err u111))
(define-constant ERR_INSUFFICIENT_LIQUIDITY (err u112))
(define-constant ERR_INVALID_PRICE (err u113))

;; ============================================
;; Protocol Constants
;; ============================================
(define-constant LTV_PERCENTAGE u70)
(define-constant INTEREST_RATE_PERCENTAGE u10)
(define-constant LIQUIDATION_THRESHOLD_PERCENTAGE u100)
(define-constant LIQUIDATION_PENALTY_PERCENTAGE u10)
(define-constant ONE_YEAR_IN_SECS u31556952)
(define-constant DEFAULT_ADMIN 'SP1G4ZDXED8XM2XJ4Q4GJ7F4PG4EJQ1KKXRCD0S3K)
(define-constant SBTC_DEPOSIT_CONTRACT .sbtc-deposit-dummy-v2)

;; ============================================
;; Data Variables
;; ============================================
(define-data-var total-sbtc-collateral uint u0)
(define-data-var total-stx-deposits uint u0)
(define-data-var total-stx-borrows uint u0)
(define-data-var last-interest-accrual uint u0)
(define-data-var cumulative-yield-bips uint u0)
(define-data-var admin principal DEFAULT_ADMIN)
(define-data-var paused bool false)
(define-data-var ltv-percentage uint LTV_PERCENTAGE)
(define-data-var interest-rate-percentage uint INTEREST_RATE_PERCENTAGE)
(define-data-var liquidation-threshold-percentage uint LIQUIDATION_THRESHOLD_PERCENTAGE)
(define-data-var liquidation-penalty-percentage uint LIQUIDATION_PENALTY_PERCENTAGE)

;; ============================================
;; Maps
;; ============================================
(define-map collateral
  { user: principal }
  { amount: uint }
)

(define-map deposits
  { user: principal }
  {
    amount: uint,
    yield-index: uint,
  }
)

(define-map borrows
  { user: principal }
  {
    amount: uint,
    last-accrued: uint,
  }
)

;; ============================================
;; Public Functions
;; ============================================
(define-public (get-sbtc-stx-price)
  (contract-call? .mock-oracle-v4 get-price)
)

(define-public (deposit-stx (amount uint))
  (let (
      (existing (map-get? deposits { user: tx-sender }))
      (deposited-stx (default-to u0 (get amount existing)))
    )
    (asserts! (is-eq (var-get paused) false) ERR_PAUSED)
    (asserts! (> amount u0) ERR_INVALID_DEPOSIT_AMOUNT)

    (unwrap-panic (accrue-interest))

    (unwrap! (stx-transfer? amount tx-sender (contract-principal)) (err u1))

    (map-set deposits
      { user: tx-sender }
      {
        amount: (+ deposited-stx amount),
        yield-index: (var-get cumulative-yield-bips)
      }
    )

    (var-set total-stx-deposits (+ (var-get total-stx-deposits) amount))

    (ok true)
  )
)

(define-public (withdraw-stx (amount uint))
  (let (
      (deposit (unwrap! (map-get? deposits { user: tx-sender }) ERR_INVALID_WITHDRAW_AMOUNT))
      (deposited-stx (get amount deposit))
      (caller tx-sender)
    )
    (asserts! (is-eq (var-get paused) false) ERR_PAUSED)
    (asserts! (> amount u0) ERR_INVALID_WITHDRAW_AMOUNT)
    (asserts! (>= deposited-stx amount) ERR_INVALID_WITHDRAW_AMOUNT)
    
    (unwrap-panic (accrue-interest))
    
    (let (
        (pending-yield (unwrap-panic (get-pending-yield tx-sender)))
        (new-amount (- deposited-stx amount))
      )
      (if (is-eq new-amount u0)
        (map-delete deposits { user: tx-sender })
        (map-set deposits
          { user: tx-sender }
          {
            amount: new-amount,
            yield-index: (var-get cumulative-yield-bips)
          }
        )
      )

      (var-set total-stx-deposits (- (var-get total-stx-deposits) amount))

      (asserts! (>= (get-contract-balance) (+ amount pending-yield)) ERR_INSUFFICIENT_LIQUIDITY)
      (unwrap!
        (as-contract (stx-transfer? (+ amount pending-yield) (contract-principal) caller))
        (err u1)
      )

      (ok true)
    )
  )
)

(define-public (borrow-stx
    (collateral-amount uint)
    (amount-stx uint)
  )
  (let (
      (existing-borrow (map-get? borrows { user: tx-sender }))
      (existing-collateral (map-get? collateral { user: tx-sender }))
      (caller tx-sender)
    )
    (asserts! (is-eq (var-get paused) false) ERR_PAUSED)
    (asserts! (> amount-stx u0) ERR_INVALID_BORROW_AMOUNT)
    (asserts!
      (or (> collateral-amount u0) (> (default-to u0 (get amount existing-collateral)) u0))
      ERR_EXCEEDED_MAX_BORROW
    )

    (unwrap-panic (accrue-interest))

    (let (
        (existing-collateral-amount (default-to u0 (get amount existing-collateral)))
        (total-collateral (+ existing-collateral-amount collateral-amount))
        (price (unwrap-panic (get-sbtc-stx-price)))
        (collateral-value total-collateral)
        (max-borrow (/ (* (* collateral-value price) (var-get ltv-percentage)) u100))
        (current-borrow (default-to u0 (get amount existing-borrow)))
        (last-accrued (default-to (get-current-time) (get last-accrued existing-borrow)))
        (interest (unwrap-panic (calculate-interest current-borrow last-accrued)))
        (new-borrow (+ (+ current-borrow interest) amount-stx))
      )
      (asserts! (> price u0) ERR_INVALID_PRICE)
      (asserts! (<= new-borrow max-borrow) ERR_EXCEEDED_MAX_BORROW)
      (asserts! (>= (get-contract-balance) amount-stx) ERR_INSUFFICIENT_LIQUIDITY)

      (if (> collateral-amount u0)
        (unwrap!
          (contract-call? SBTC_DEPOSIT_CONTRACT transfer collateral-amount tx-sender (contract-principal) none)
          ERR_INVALID_SBTC_CONTRACT
        )
        true
      )

      (unwrap!
        (as-contract (stx-transfer? amount-stx (contract-principal) caller))
        (err u1)
      )

      (map-set collateral
        { user: tx-sender }
        { amount: total-collateral }
      )
      (map-set borrows
        { user: tx-sender }
        {
          amount: new-borrow,
          last-accrued: (get-current-time)
        }
      )

      (var-set total-stx-borrows (+ (var-get total-stx-borrows) amount-stx))
      (if (> collateral-amount u0)
        (var-set total-sbtc-collateral (+ (var-get total-sbtc-collateral) collateral-amount))
        true
      )

      (ok true)
    )
  )
)

(define-public (repay)
  (let (
      (borrow (unwrap! (map-get? borrows { user: tx-sender }) ERR_INVALID_WITHDRAW_AMOUNT))
      (borrowed (get amount borrow))
      (last-accrued (get last-accrued borrow))
      (collateral-entry (map-get? collateral { user: tx-sender }))
      (collateral-amount (default-to u0 (get amount collateral-entry)))
      (caller tx-sender)
    )
    (asserts! (is-eq (var-get paused) false) ERR_PAUSED)
    (asserts! (> borrowed u0) ERR_INVALID_WITHDRAW_AMOUNT)

    (let (
        (interest (unwrap-panic (calculate-interest borrowed last-accrued)))
        (total-owed (+ borrowed interest))
      )
      (unwrap! (stx-transfer? total-owed tx-sender (contract-principal)) (err u1))

      (if (> collateral-amount u0)
        (unwrap!
          (as-contract (contract-call? SBTC_DEPOSIT_CONTRACT transfer collateral-amount (contract-principal) caller none))
          ERR_INVALID_SBTC_CONTRACT
        )
        true
      )

      (map-delete borrows { user: tx-sender })
      (map-delete collateral { user: tx-sender })
      (var-set total-stx-borrows (- (var-get total-stx-borrows) total-owed))
      (var-set total-sbtc-collateral (- (var-get total-sbtc-collateral) collateral-amount))

      (ok true)
    )
  )
)

(define-public (liquidate (user principal))
  (let (
      (borrow (unwrap! (map-get? borrows { user: user }) ERR_CANNOT_BE_LIQUIDATED))
      (borrowed (get amount borrow))
      (last-accrued (get last-accrued borrow))
      (collateral-entry (unwrap! (map-get? collateral { user: user }) ERR_CANNOT_BE_LIQUIDATED))
      (collateral-amount (get amount collateral-entry))
      (liquidator tx-sender)
    )
    (asserts! (is-eq (var-get paused) false) ERR_PAUSED)
    (let (
        (interest (unwrap-panic (calculate-interest borrowed last-accrued)))
      (total-owed (+ borrowed interest))
      (price (unwrap-panic (get-sbtc-stx-price)))
      (collateral-value (* collateral-amount price))
      (threshold (var-get liquidation-threshold-percentage))
      (penalty (var-get liquidation-penalty-percentage))
    )
    (asserts! (> price u0) ERR_INVALID_PRICE)
    (asserts! (>= (* total-owed u100) (* collateral-value threshold)) ERR_CANNOT_BE_LIQUIDATED)

      (let (
          (repay-amount (/ (* total-owed (- u100 penalty)) u100))
        )
        (unwrap! (stx-transfer? repay-amount tx-sender (contract-principal)) (err u1))
        (unwrap!
          (as-contract (contract-call? SBTC_DEPOSIT_CONTRACT transfer collateral-amount (contract-principal) liquidator none))
          ERR_INVALID_SBTC_CONTRACT
        )

        (map-delete borrows { user: user })
        (map-delete collateral { user: user })
        (var-set total-stx-borrows (- (var-get total-stx-borrows) total-owed))
        (var-set total-sbtc-collateral (- (var-get total-sbtc-collateral) collateral-amount))

        (ok true)
      )
    )
  )
)

;; ============================================
;; Read-Only Functions
;; ============================================
(define-read-only (get-pending-yield (user principal))
  (let (
      (deposit (map-get? deposits { user: user }))
      (amount-stx (default-to u0 (get amount deposit)))
      (yield-index (default-to u0 (get yield-index deposit)))
    )
    (let (
        (delta (- (var-get cumulative-yield-bips) yield-index))
        (pending-yield (/ (* amount-stx delta) u10000))
      )
      (ok pending-yield)
    )
  )
)

(define-read-only (get-debt (user principal))
  (let (
      (borrow (map-get? borrows { user: user }))
      (amount (default-to u0 (get amount borrow)))
      (last-accrued (default-to (get-current-time) (get last-accrued borrow)))
    )
    (let (
        (interest (unwrap-panic (calculate-interest amount last-accrued)))
      )
      (ok (+ amount interest))
    )
  )
)

(define-read-only (get-collateral (user principal))
  (let (
      (entry (map-get? collateral { user: user }))
      (amount (default-to u0 (get amount entry)))
    )
    (ok amount)
  )
)

(define-read-only (get-params)
  (ok {
    ltv: (var-get ltv-percentage),
    interest: (var-get interest-rate-percentage),
    liquidation-threshold: (var-get liquidation-threshold-percentage),
    liquidation-penalty: (var-get liquidation-penalty-percentage)
  })
)


;; ============================================
;; Admin Functions
;; ============================================
(define-public (set-paused (paused-value bool))
  (begin
    (asserts! (is-eq tx-sender (var-get admin)) ERR_UNAUTHORIZED)
    (var-set paused paused-value)
    (ok true)
  )
)

(define-public (set-admin (new-admin principal))
  (begin
    (asserts! (is-eq tx-sender (var-get admin)) ERR_UNAUTHORIZED)
    (var-set admin new-admin)
    (ok true)
  )
)

(define-public (set-params
    (new-ltv uint)
    (new-interest uint)
    (new-threshold uint)
    (new-penalty uint)
  )
  (begin
    (asserts! (is-eq tx-sender (var-get admin)) ERR_UNAUTHORIZED)
    (asserts! (<= new-ltv u90) ERR_INVALID_LIQUIDATION)
    (asserts! (<= new-interest u100) ERR_INVALID_LIQUIDATION)
    (asserts! (<= new-threshold u150) ERR_INVALID_LIQUIDATION)
    (asserts! (<= new-penalty u30) ERR_INVALID_LIQUIDATION)
    (var-set ltv-percentage new-ltv)
    (var-set interest-rate-percentage new-interest)
    (var-set liquidation-threshold-percentage new-threshold)
    (var-set liquidation-penalty-percentage new-penalty)
    (ok true)
  )
)


;; ============================================
;; Private Functions
;; ============================================
(define-private (contract-principal)
  (as-contract tx-sender)
)

(define-private (get-contract-balance)
  (stx-get-balance (contract-principal))
)

(define-private (get-current-time)
  u0
)

(define-private (accrue-interest)
  (let (
      (current-time (get-current-time))
      (last-accrual (var-get last-interest-accrual))
    )
    (if (is-eq last-accrual u0)
      (begin
        (var-set last-interest-accrual current-time)
        (ok true)
      )
      (let (
          (dt (- current-time last-accrual))
        )
    (if (> dt u0)
      (let (
          (total-borrows (var-get total-stx-borrows))
          (interest-numerator (* (* total-borrows (var-get interest-rate-percentage)) dt))
          (interest-denominator (* ONE_YEAR_IN_SECS u100))
          (interest (/ interest-numerator interest-denominator))
          (total-deposits (var-get total-stx-deposits))
          (new-yield (if (> total-deposits u0) (/ (* interest u10000) total-deposits) u0))
        )
        (var-set last-interest-accrual current-time)
        (var-set total-stx-borrows (+ total-borrows interest))
        (var-set cumulative-yield-bips
          (+ (var-get cumulative-yield-bips) new-yield)
        )
        (ok true)
      )
      (ok true)
    )
      )
    )
  )
)

(define-private (calculate-interest (principal uint) (last-accrued uint))
  (let (
      (current-time (get-current-time))
      (dt (- current-time last-accrued))
    )
    (if (> dt u0)
      (let (
          (rate (var-get interest-rate-percentage))
          (numerator (* principal (* rate dt)))
          (denominator (* ONE_YEAR_IN_SECS u100))
        )
        (ok (/ numerator denominator))
      )
      (ok u0)
    )
  )
)

Functions (16)

FunctionAccessArgs
get-sbtc-stx-pricepublic
deposit-stxpublicamount: uint
withdraw-stxpublicamount: uint
repaypublic
liquidatepublicuser: principal
get-pending-yieldread-onlyuser: principal
get-debtread-onlyuser: principal
get-collateralread-onlyuser: principal
get-paramsread-only
set-pausedpublicpaused-value: bool
set-adminpublicnew-admin: principal
contract-principalprivate
get-contract-balanceprivate
get-current-timeprivate
accrue-interestprivate
calculate-interestprivateprincipal: uint, last-accrued: uint