Source Code

;; halo-vault-v2.clar
;; Multi-asset collateral vault with per-asset LTV and yield
;;
;; Features:
;; - Deposits in hUSD (80% LTV), STX (50% LTV), sBTC (50% LTV)
;; - Per-asset Synthetix-style yield accumulator
;; - Aggregate USD capacity across all assets for circle participation
;; - Authorized contracts can lock/release/slash collateral
;; - Admin price oracle for cross-asset USD conversion
;;
;; Dependencies: halo-sip010-trait

(use-trait ft-trait .halo-sip010-trait.sip-010-trait)

;; ============================================
;; CONSTANTS
;; ============================================

(define-constant CONTRACT_OWNER tx-sender)

(define-constant ERR_NOT_AUTHORIZED (err u700))
(define-constant ERR_INVALID_AMOUNT (err u701))
(define-constant ERR_INSUFFICIENT_BALANCE (err u702))
(define-constant ERR_INSUFFICIENT_CAPACITY (err u703))
(define-constant ERR_NO_DEPOSIT (err u704))
(define-constant ERR_TRANSFER_FAILED (err u705))
(define-constant ERR_INVALID_PARAMS (err u706))
(define-constant ERR_TOKEN_NOT_SUPPORTED (err u707))
(define-constant ERR_COMMITMENT_NOT_FOUND (err u708))
(define-constant ERR_ZERO_PRICE (err u709))
(define-constant ERR_ALREADY_AUTHORIZED (err u710))
(define-constant ERR_ASSET_NOT_FOUND (err u711))
(define-constant ERR_ASSET_NOT_ACTIVE (err u712))
(define-constant ERR_VAULT_PAUSED (err u713))
(define-constant ERR_RATE_TOO_HIGH (err u714))

;; Asset type constants
(define-constant ASSET_TYPE_HUSD u0)
(define-constant ASSET_TYPE_STX  u1)
(define-constant ASSET_TYPE_SBTC u2)

;; LTV denominator
(define-constant LTV_DENOMINATOR u10000)

;; Yield precision (10^12)
(define-constant PRECISION u1000000000000)

;; Price precision (6 decimals: $1.00 = u1000000)
(define-constant PRICE_PRECISION u1000000)

;; Minimum yield funding duration (~1 day in blocks)
(define-constant MIN_YIELD_DURATION u144)

;; ============================================
;; DATA VARIABLES
;; ============================================

(define-data-var admin principal CONTRACT_OWNER)
(define-data-var authorized-contracts (list 10 principal) (list))
(define-data-var vault-paused bool false)

;; ============================================
;; DATA MAPS
;; ============================================

;; Per-asset configuration and yield accumulator
(define-map supported-assets uint {
  token-principal: (optional principal),  ;; none for STX (native)
  ltv-ratio: uint,                        ;; basis points (8000 = 80%)
  price-usd: uint,                        ;; 6 decimal USD price
  decimals: uint,                         ;; token native decimals
  price-last-updated: uint,
  is-active: bool,
  ;; Per-asset Synthetix yield accumulator
  reward-per-token-stored: uint,
  last-update-block: uint,
  reward-rate: uint,
  reward-end-block: uint,
  total-deposited: uint
})

;; Per-user, per-asset balances and yield tracking
(define-map user-asset-deposits { user: principal, asset-type: uint } {
  deposited: uint,
  reward-per-token-paid: uint,
  rewards-earned: uint
})

;; Aggregate committed USD across all circles for a user
(define-map user-committed principal {
  total-committed-usd: uint
})

;; Per-circle commitment tracking
(define-map circle-commitments-v2 { user: principal, circle-id: uint } {
  commitment-usd: uint
})

;; ============================================
;; PRIVATE FUNCTIONS
;; ============================================

(define-private (min-uint (a uint) (b uint))
  (if (<= a b) a b)
)

(define-private (is-authorized-caller (caller principal))
  (or (is-eq caller (var-get admin))
      (is-some (index-of? (var-get authorized-contracts) caller)))
)

;; Get or create default user deposit for an asset
(define-private (get-or-create-user-deposit (user principal) (asset-type uint))
  (default-to {
    deposited: u0,
    reward-per-token-paid: (get-asset-rpt asset-type),
    rewards-earned: u0
  } (map-get? user-asset-deposits { user: user, asset-type: asset-type }))
)

;; Get asset's current reward-per-token-stored
(define-private (get-asset-rpt (asset-type uint))
  (match (map-get? supported-assets asset-type)
    asset (get reward-per-token-stored asset)
    u0
  )
)

;; Update per-asset yield accumulator (call before deposit/withdraw/claim)
(define-private (update-asset-reward (asset-type uint))
  (match (map-get? supported-assets asset-type)
    asset (let (
      (current-block stacks-block-height)
      (applicable-block (min-uint current-block (get reward-end-block asset)))
      (total (get total-deposited asset))
      (stored (get reward-per-token-stored asset))
      (last-block (get last-update-block asset))
    )
      (if (and (> total u0) (> applicable-block last-block))
        (let (
          (elapsed (- applicable-block last-block))
          (new-rewards (* elapsed (get reward-rate asset)))
          (additional (/ (* new-rewards PRECISION) total))
        )
          (map-set supported-assets asset-type
            (merge asset {
              reward-per-token-stored: (+ stored additional),
              last-update-block: applicable-block
            })
          )
        )
        (map-set supported-assets asset-type
          (merge asset { last-update-block: applicable-block })
        )
      )
    )
    true ;; Asset not found -- no-op
  )
)

;; Update user's reward snapshot for a specific asset
(define-private (update-user-asset-reward (user principal) (asset-type uint))
  (match (map-get? user-asset-deposits { user: user, asset-type: asset-type })
    dep (let (
      (deposited (get deposited dep))
      (paid (get reward-per-token-paid dep))
      (earned (get rewards-earned dep))
      (current-rpt (get-asset-rpt asset-type))
      (new-earned (+ earned (/ (* deposited (- current-rpt paid)) PRECISION)))
    )
      (map-set user-asset-deposits { user: user, asset-type: asset-type }
        (merge dep {
          rewards-earned: new-earned,
          reward-per-token-paid: current-rpt
        })
      )
    )
    true ;; No deposit, nothing to update
  )
)

;; Calculate USD capacity for a single asset deposit
(define-private (calculate-asset-capacity-usd (asset-type uint) (deposited uint))
  (match (map-get? supported-assets asset-type)
    asset (let (
      (price (get price-usd asset))
      (decimals (get decimals asset))
      (ltv (get ltv-ratio asset))
      ;; USD value = (deposited * price) / 10^decimals
      (usd-value (/ (* deposited price) (pow u10 decimals)))
      ;; Capacity = usd-value * ltv / 10000
    )
      (/ (* usd-value ltv) LTV_DENOMINATOR)
    )
    u0
  )
)

;; ============================================
;; READ-ONLY FUNCTIONS
;; ============================================

;; Get asset configuration
(define-read-only (get-asset-config (asset-type uint))
  (map-get? supported-assets asset-type)
)

;; Get user's deposit for a specific asset
(define-read-only (get-user-deposit (user principal) (asset-type uint))
  (map-get? user-asset-deposits { user: user, asset-type: asset-type })
)

;; Get user's committed amount
(define-read-only (get-user-committed (user principal))
  (default-to { total-committed-usd: u0 }
    (map-get? user-committed user))
)

;; Get circle commitment
(define-read-only (get-circle-commitment (user principal) (circle-id uint))
  (map-get? circle-commitments-v2 { user: user, circle-id: circle-id })
)

;; Calculate total USD capacity across all assets for a user
(define-read-only (get-total-capacity (user principal))
  (let (
    ;; hUSD capacity
    (husd-dep (default-to { deposited: u0, reward-per-token-paid: u0, rewards-earned: u0 }
      (map-get? user-asset-deposits { user: user, asset-type: ASSET_TYPE_HUSD })))
    (husd-cap (calculate-asset-capacity-usd ASSET_TYPE_HUSD (get deposited husd-dep)))
    ;; STX capacity
    (stx-dep (default-to { deposited: u0, reward-per-token-paid: u0, rewards-earned: u0 }
      (map-get? user-asset-deposits { user: user, asset-type: ASSET_TYPE_STX })))
    (stx-cap (calculate-asset-capacity-usd ASSET_TYPE_STX (get deposited stx-dep)))
    ;; sBTC capacity
    (sbtc-dep (default-to { deposited: u0, reward-per-token-paid: u0, rewards-earned: u0 }
      (map-get? user-asset-deposits { user: user, asset-type: ASSET_TYPE_SBTC })))
    (sbtc-cap (calculate-asset-capacity-usd ASSET_TYPE_SBTC (get deposited sbtc-dep)))
  )
    (ok (+ husd-cap (+ stx-cap sbtc-cap)))
  )
)

;; Get available capacity (total - committed)
(define-read-only (get-available-capacity (user principal))
  (let (
    (total-cap (unwrap-panic (get-total-capacity user)))
    (committed (get total-committed-usd (get-user-committed user)))
  )
    (ok (if (> total-cap committed) (- total-cap committed) u0))
  )
)

;; Check if user can commit additional USD amount
(define-read-only (can-commit (user principal) (additional-usd uint))
  (let (
    (available (unwrap-panic (get-available-capacity user)))
  )
    (ok (>= available additional-usd))
  )
)

;; Get pending yield for a user on a specific asset (view, no state change)
(define-read-only (get-pending-yield (user principal) (asset-type uint))
  (match (map-get? user-asset-deposits { user: user, asset-type: asset-type })
    dep (match (map-get? supported-assets asset-type)
      asset (let (
        (deposited (get deposited dep))
        (paid (get reward-per-token-paid dep))
        (earned (get rewards-earned dep))
        (stored (get reward-per-token-stored asset))
        (total (get total-deposited asset))
        (current-block stacks-block-height)
        (applicable-block (min-uint current-block (get reward-end-block asset)))
        (last-block (get last-update-block asset))
        (current-rpt (if (and (> total u0) (> applicable-block last-block))
          (+ stored (/ (* (* (- applicable-block last-block) (get reward-rate asset)) PRECISION) total))
          stored
        ))
      )
        (+ earned (/ (* deposited (- current-rpt paid)) PRECISION))
      )
      u0
    )
    u0
  )
)

;; Calculate commitment USD for a circle (contribution * members, converted to USD)
(define-read-only (calculate-commitment-usd
  (contribution uint)
  (total-members uint)
  (asset-type uint)
)
  (match (map-get? supported-assets asset-type)
    asset (let (
      (price (get price-usd asset))
      (decimals (get decimals asset))
      (total-obligation (* contribution total-members))
      (commitment-usd (/ (* total-obligation price) (pow u10 decimals)))
    )
      (ok commitment-usd)
    )
    ERR_ASSET_NOT_FOUND
  )
)

;; Get admin
(define-read-only (get-admin)
  (var-get admin)
)

;; Check if a contract is authorized
(define-read-only (is-authorized (caller principal))
  (is-authorized-caller caller)
)

;; Get vault summary for a user
(define-read-only (get-vault-summary (user principal))
  (let (
    (husd-dep (default-to { deposited: u0, reward-per-token-paid: u0, rewards-earned: u0 }
      (map-get? user-asset-deposits { user: user, asset-type: ASSET_TYPE_HUSD })))
    (stx-dep (default-to { deposited: u0, reward-per-token-paid: u0, rewards-earned: u0 }
      (map-get? user-asset-deposits { user: user, asset-type: ASSET_TYPE_STX })))
    (sbtc-dep (default-to { deposited: u0, reward-per-token-paid: u0, rewards-earned: u0 }
      (map-get? user-asset-deposits { user: user, asset-type: ASSET_TYPE_SBTC })))
    (committed-data (get-user-committed user))
  )
    {
      husd-deposited: (get deposited husd-dep),
      stx-deposited: (get deposited stx-dep),
      sbtc-deposited: (get deposited sbtc-dep),
      total-committed-usd: (get total-committed-usd committed-data),
      total-capacity-usd: (unwrap-panic (get-total-capacity user)),
      available-capacity-usd: (unwrap-panic (get-available-capacity user))
    }
  )
)

;; ============================================
;; PUBLIC FUNCTIONS -- DEPOSITS
;; ============================================

;; Deposit hUSD stablecoin
(define-public (deposit-husd (token <ft-trait>) (amount uint))
  (let (
    (caller tx-sender)
    (asset (unwrap! (map-get? supported-assets ASSET_TYPE_HUSD) ERR_ASSET_NOT_FOUND))
    (expected-token (unwrap! (get token-principal asset) ERR_TOKEN_NOT_SUPPORTED))
  )
    (asserts! (get is-active asset) ERR_ASSET_NOT_ACTIVE)
    (asserts! (is-eq (contract-of token) expected-token) ERR_TOKEN_NOT_SUPPORTED)
    (asserts! (> amount u0) ERR_INVALID_AMOUNT)
    (asserts! (not (var-get vault-paused)) ERR_VAULT_PAUSED)
    (asserts! (> (get price-usd asset) u0) ERR_ZERO_PRICE)

    ;; Update yield
    (update-asset-reward ASSET_TYPE_HUSD)
    (update-user-asset-reward caller ASSET_TYPE_HUSD)

    ;; Update user deposit (effects before interactions)
    (let (
      (dep (get-or-create-user-deposit caller ASSET_TYPE_HUSD))
    )
      (map-set user-asset-deposits { user: caller, asset-type: ASSET_TYPE_HUSD }
        (merge dep { deposited: (+ (get deposited dep) amount) })
      )
    )

    ;; Update asset total (effects before interactions)
    (map-set supported-assets ASSET_TYPE_HUSD
      (merge (unwrap-panic (map-get? supported-assets ASSET_TYPE_HUSD))
        { total-deposited: (+ (get total-deposited asset) amount) })
    )

    ;; Transfer tokens (interactions last)
    (try! (contract-call? token transfer amount caller (as-contract tx-sender) none))

    (print { event: "vault-v2-deposit", user: caller, asset-type: ASSET_TYPE_HUSD, amount: amount })
    (ok true)
  )
)

;; Deposit native STX
(define-public (deposit-stx (amount uint))
  (let (
    (caller tx-sender)
    (asset (unwrap! (map-get? supported-assets ASSET_TYPE_STX) ERR_ASSET_NOT_FOUND))
  )
    (asserts! (get is-active asset) ERR_ASSET_NOT_ACTIVE)
    (asserts! (> amount u0) ERR_INVALID_AMOUNT)
    (asserts! (not (var-get vault-paused)) ERR_VAULT_PAUSED)
    (asserts! (> (get price-usd asset) u0) ERR_ZERO_PRICE)

    ;; Update yield
    (update-asset-reward ASSET_TYPE_STX)
    (update-user-asset-reward caller ASSET_TYPE_STX)

    ;; Update user deposit (effects before interactions)
    (let (
      (dep (get-or-create-user-deposit caller ASSET_TYPE_STX))
    )
      (map-set user-asset-deposits { user: caller, asset-type: ASSET_TYPE_STX }
        (merge dep { deposited: (+ (get deposited dep) amount) })
      )
    )

    ;; Update asset total (effects before interactions)
    (map-set supported-assets ASSET_TYPE_STX
      (merge (unwrap-panic (map-get? supported-assets ASSET_TYPE_STX))
        { total-deposited: (+ (get total-deposited asset) amount) })
    )

    ;; Transfer STX (interactions last)
    (try! (stx-transfer? amount caller (as-contract tx-sender)))

    (print { event: "vault-v2-deposit", user: caller, asset-type: ASSET_TYPE_STX, amount: amount })
    (ok true)
  )
)

;; Deposit sBTC
(define-public (deposit-sbtc (token <ft-trait>) (amount uint))
  (let (
    (caller tx-sender)
    (asset (unwrap! (map-get? supported-assets ASSET_TYPE_SBTC) ERR_ASSET_NOT_FOUND))
    (expected-token (unwrap! (get token-principal asset) ERR_TOKEN_NOT_SUPPORTED))
  )
    (asserts! (get is-active asset) ERR_ASSET_NOT_ACTIVE)
    (asserts! (is-eq (contract-of token) expected-token) ERR_TOKEN_NOT_SUPPORTED)
    (asserts! (> amount u0) ERR_INVALID_AMOUNT)
    (asserts! (not (var-get vault-paused)) ERR_VAULT_PAUSED)
    (asserts! (> (get price-usd asset) u0) ERR_ZERO_PRICE)

    ;; Update yield
    (update-asset-reward ASSET_TYPE_SBTC)
    (update-user-asset-reward caller ASSET_TYPE_SBTC)

    ;; Update user deposit (effects before interactions)
    (let (
      (dep (get-or-create-user-deposit caller ASSET_TYPE_SBTC))
    )
      (map-set user-asset-deposits { user: caller, asset-type: ASSET_TYPE_SBTC }
        (merge dep { deposited: (+ (get deposited dep) amount) })
      )
    )

    ;; Update asset total (effects before interactions)
    (map-set supported-assets ASSET_TYPE_SBTC
      (merge (unwrap-panic (map-get? supported-assets ASSET_TYPE_SBTC))
        { total-deposited: (+ (get total-deposited asset) amount) })
    )

    ;; Transfer tokens (interactions last)
    (try! (contract-call? token transfer amount caller (as-contract tx-sender) none))

    (print { event: "vault-v2-deposit", user: caller, asset-type: ASSET_TYPE_SBTC, amount: amount })
    (ok true)
  )
)

;; ============================================
;; PUBLIC FUNCTIONS -- WITHDRAWALS
;; ============================================

;; Internal withdrawal check: ensures remaining capacity covers commitments
(define-private (check-withdrawal-allowed (user principal) (asset-type uint) (amount uint))
  (let (
    (dep (unwrap! (map-get? user-asset-deposits { user: user, asset-type: asset-type }) ERR_NO_DEPOSIT))
    (deposited (get deposited dep))
  )
    (asserts! (<= amount deposited) ERR_INSUFFICIENT_BALANCE)
    ;; Calculate what total capacity would be after withdrawal
    (let (
      (husd-dep-amt (if (is-eq asset-type ASSET_TYPE_HUSD)
        (- deposited amount)
        (get deposited (default-to { deposited: u0, reward-per-token-paid: u0, rewards-earned: u0 }
          (map-get? user-asset-deposits { user: user, asset-type: ASSET_TYPE_HUSD })))))
      (stx-dep-amt (if (is-eq asset-type ASSET_TYPE_STX)
        (- deposited amount)
        (get deposited (default-to { deposited: u0, reward-per-token-paid: u0, rewards-earned: u0 }
          (map-get? user-asset-deposits { user: user, asset-type: ASSET_TYPE_STX })))))
      (sbtc-dep-amt (if (is-eq asset-type ASSET_TYPE_SBTC)
        (- deposited amount)
        (get deposited (default-to { deposited: u0, reward-per-token-paid: u0, rewards-earned: u0 }
          (map-get? user-asset-deposits { user: user, asset-type: ASSET_TYPE_SBTC })))))
      (new-total-cap (+
        (calculate-asset-capacity-usd ASSET_TYPE_HUSD husd-dep-amt)
        (+ (calculate-asset-capacity-usd ASSET_TYPE_STX stx-dep-amt)
           (calculate-asset-capacity-usd ASSET_TYPE_SBTC sbtc-dep-amt))))
      (committed (get total-committed-usd (get-user-committed user)))
    )
      (asserts! (>= new-total-cap committed) ERR_INSUFFICIENT_CAPACITY)
      (ok true)
    )
  )
)

;; Withdraw hUSD
(define-public (withdraw-husd (token <ft-trait>) (amount uint))
  (let (
    (caller tx-sender)
    (asset (unwrap! (map-get? supported-assets ASSET_TYPE_HUSD) ERR_ASSET_NOT_FOUND))
    (expected-token (unwrap! (get token-principal asset) ERR_TOKEN_NOT_SUPPORTED))
  )
    (asserts! (is-eq (contract-of token) expected-token) ERR_TOKEN_NOT_SUPPORTED)
    (asserts! (> amount u0) ERR_INVALID_AMOUNT)
    (asserts! (not (var-get vault-paused)) ERR_VAULT_PAUSED)

    ;; Update yield
    (update-asset-reward ASSET_TYPE_HUSD)
    (update-user-asset-reward caller ASSET_TYPE_HUSD)

    ;; Check withdrawal allowed
    (try! (check-withdrawal-allowed caller ASSET_TYPE_HUSD amount))

    ;; Update deposit BEFORE transfer (checks-effects-interactions)
    (let (
      (dep (unwrap-panic (map-get? user-asset-deposits { user: caller, asset-type: ASSET_TYPE_HUSD })))
    )
      (map-set user-asset-deposits { user: caller, asset-type: ASSET_TYPE_HUSD }
        (merge dep { deposited: (- (get deposited dep) amount) })
      )
    )

    ;; Update asset total BEFORE transfer
    (map-set supported-assets ASSET_TYPE_HUSD
      (merge (unwrap-panic (map-get? supported-assets ASSET_TYPE_HUSD))
        { total-deposited: (- (get total-deposited asset) amount) })
    )

    ;; Transfer (interactions last)
    (try! (as-contract (contract-call? token transfer amount tx-sender caller none)))

    (print { event: "vault-v2-withdraw", user: caller, asset-type: ASSET_TYPE_HUSD, amount: amount })
    (ok true)
  )
)

;; Withdraw STX
(define-public (withdraw-stx (amount uint))
  (let (
    (caller tx-sender)
    (asset (unwrap! (map-get? supported-assets ASSET_TYPE_STX) ERR_ASSET_NOT_FOUND))
  )
    (asserts! (> amount u0) ERR_INVALID_AMOUNT)
    (asserts! (not (var-get vault-paused)) ERR_VAULT_PAUSED)

    ;; Update yield
    (update-asset-reward ASSET_TYPE_STX)
    (update-user-asset-reward caller ASSET_TYPE_STX)

    ;; Check withdrawal allowed
    (try! (check-withdrawal-allowed caller ASSET_TYPE_STX amount))

    ;; Update deposit BEFORE transfer (checks-effects-interactions)
    (let (
      (dep (unwrap-panic (map-get? user-asset-deposits { user: caller, asset-type: ASSET_TYPE_STX })))
    )
      (map-set user-asset-deposits { user: caller, asset-type: ASSET_TYPE_STX }
        (merge dep { deposited: (- (get deposited dep) amount) })
      )
    )

    ;; Update asset total BEFORE transfer
    (map-set supported-assets ASSET_TYPE_STX
      (merge (unwrap-panic (map-get? supported-assets ASSET_TYPE_STX))
        { total-deposited: (- (get total-deposited asset) amount) })
    )

    ;; Transfer STX back (interactions last)
    (try! (as-contract (stx-transfer? amount tx-sender caller)))

    (print { event: "vault-v2-withdraw", user: caller, asset-type: ASSET_TYPE_STX, amount: amount })
    (ok true)
  )
)

;; Withdraw sBTC
(define-public (withdraw-sbtc (token <ft-trait>) (amount uint))
  (let (
    (caller tx-sender)
    (asset (unwrap! (map-get? supported-assets ASSET_TYPE_SBTC) ERR_ASSET_NOT_FOUND))
    (expected-token (unwrap! (get token-principal asset) ERR_TOKEN_NOT_SUPPORTED))
  )
    (asserts! (is-eq (contract-of token) expected-token) ERR_TOKEN_NOT_SUPPORTED)
    (asserts! (> amount u0) ERR_INVALID_AMOUNT)
    (asserts! (not (var-get vault-paused)) ERR_VAULT_PAUSED)

    ;; Update yield
    (update-asset-reward ASSET_TYPE_SBTC)
    (update-user-asset-reward caller ASSET_TYPE_SBTC)

    ;; Check withdrawal allowed
    (try! (check-withdrawal-allowed caller ASSET_TYPE_SBTC amount))

    ;; Update deposit BEFORE transfer (checks-effects-interactions)
    (let (
      (dep (unwrap-panic (map-get? user-asset-deposits { user: caller, asset-type: ASSET_TYPE_SBTC })))
    )
      (map-set user-asset-deposits { user: caller, asset-type: ASSET_TYPE_SBTC }
        (merge dep { deposited: (- (get deposited dep) amount) })
      )
    )

    ;; Update asset total BEFORE transfer
    (map-set supported-assets ASSET_TYPE_SBTC
      (merge (unwrap-panic (map-get? supported-assets ASSET_TYPE_SBTC))
        { total-deposited: (- (get total-deposited asset) amount) })
    )

    ;; Transfer (interactions last)
    (try! (as-contract (contract-call? token transfer amount tx-sender caller none)))

    (print { event: "vault-v2-withdraw", user: caller, asset-type: ASSET_TYPE_SBTC, amount: amount })
    (ok true)
  )
)

;; ============================================
;; PUBLIC FUNCTIONS -- YIELD
;; ============================================

;; Claim yield for hUSD deposits
(define-public (claim-yield-husd (token <ft-trait>))
  (let (
    (caller tx-sender)
    (asset (unwrap! (map-get? supported-assets ASSET_TYPE_HUSD) ERR_ASSET_NOT_FOUND))
    (expected-token (unwrap! (get token-principal asset) ERR_TOKEN_NOT_SUPPORTED))
  )
    (asserts! (is-eq (contract-of token) expected-token) ERR_TOKEN_NOT_SUPPORTED)
    (asserts! (get is-active asset) ERR_ASSET_NOT_ACTIVE)

    (update-asset-reward ASSET_TYPE_HUSD)
    (update-user-asset-reward caller ASSET_TYPE_HUSD)

    (let (
      (dep (unwrap! (map-get? user-asset-deposits { user: caller, asset-type: ASSET_TYPE_HUSD }) ERR_NO_DEPOSIT))
      (reward (get rewards-earned dep))
    )
      (asserts! (> reward u0) ERR_INVALID_AMOUNT)
      (try! (as-contract (contract-call? token transfer reward tx-sender caller none)))
      (map-set user-asset-deposits { user: caller, asset-type: ASSET_TYPE_HUSD }
        (merge dep { rewards-earned: u0 })
      )
      (print { event: "yield-claimed-v2", user: caller, asset-type: ASSET_TYPE_HUSD, amount: reward })
      (ok reward)
    )
  )
)

;; Claim yield for STX deposits (paid in STX)
(define-public (claim-yield-stx)
  (let (
    (caller tx-sender)
    (asset (unwrap! (map-get? supported-assets ASSET_TYPE_STX) ERR_ASSET_NOT_FOUND))
  )
    (asserts! (get is-active asset) ERR_ASSET_NOT_ACTIVE)

    (update-asset-reward ASSET_TYPE_STX)
    (update-user-asset-reward caller ASSET_TYPE_STX)

    (let (
      (dep (unwrap! (map-get? user-asset-deposits { user: caller, asset-type: ASSET_TYPE_STX }) ERR_NO_DEPOSIT))
      (reward (get rewards-earned dep))
    )
      (asserts! (> reward u0) ERR_INVALID_AMOUNT)
      (try! (as-contract (stx-transfer? reward tx-sender caller)))
      (map-set user-asset-deposits { user: caller, asset-type: ASSET_TYPE_STX }
        (merge dep { rewards-earned: u0 })
      )
      (print { event: "yield-claimed-v2", user: caller, asset-type: ASSET_TYPE_STX, amount: reward })
      (ok reward)
    )
  )
)

;; Claim yield for sBTC deposits
(define-public (claim-yield-sbtc (token <ft-trait>))
  (let (
    (caller tx-sender)
    (asset (unwrap! (map-get? supported-assets ASSET_TYPE_SBTC) ERR_ASSET_NOT_FOUND))
    (expected-token (unwrap! (get token-principal asset) ERR_TOKEN_NOT_SUPPORTED))
  )
    (asserts! (is-eq (contract-of token) expected-token) ERR_TOKEN_NOT_SUPPORTED)
    (asserts! (get is-active asset) ERR_ASSET_NOT_ACTIVE)

    (update-asset-reward ASSET_TYPE_SBTC)
    (update-user-asset-reward caller ASSET_TYPE_SBTC)

    (let (
      (dep (unwrap! (map-get? user-asset-deposits { user: caller, asset-type: ASSET_TYPE_SBTC }) ERR_NO_DEPOSIT))
      (reward (get rewards-earned dep))
    )
      (asserts! (> reward u0) ERR_INVALID_AMOUNT)
      (try! (as-contract (contract-call? token transfer reward tx-sender caller none)))
      (map-set user-asset-deposits { user: caller, asset-type: ASSET_TYPE_SBTC }
        (merge dep { rewards-earned: u0 })
      )
      (print { event: "yield-claimed-v2", user: caller, asset-type: ASSET_TYPE_SBTC, amount: reward })
      (ok reward)
    )
  )
)

;; ============================================
;; PUBLIC FUNCTIONS -- COLLATERAL (authorized contracts only)
;; ============================================

;; Lock collateral for a circle
(define-public (lock-collateral (user principal) (circle-id uint) (commitment-usd uint))
  (let (
    (caller contract-caller)
  )
    (asserts! (is-authorized-caller caller) ERR_NOT_AUTHORIZED)
    (asserts! (> commitment-usd u0) ERR_INVALID_AMOUNT)

    ;; Check total capacity covers new commitment
    (let (
      (total-cap (unwrap-panic (get-total-capacity user)))
      (committed-data (get-user-committed user))
      (current-committed (get total-committed-usd committed-data))
      (new-committed (+ current-committed commitment-usd))
    )
      (asserts! (<= new-committed total-cap) ERR_INSUFFICIENT_CAPACITY)

      ;; Update aggregate committed
      (map-set user-committed user { total-committed-usd: new-committed })

      ;; Record per-circle commitment
      (map-set circle-commitments-v2 { user: user, circle-id: circle-id } {
        commitment-usd: commitment-usd
      })

      (print {
        event: "collateral-locked-v2",
        user: user,
        circle-id: circle-id,
        commitment-usd: commitment-usd,
        total-committed: new-committed
      })

      (ok true)
    )
  )
)

;; Release collateral when circle completes
(define-public (release-collateral (user principal) (circle-id uint))
  (let (
    (caller contract-caller)
  )
    (asserts! (is-authorized-caller caller) ERR_NOT_AUTHORIZED)

    (let (
      (commitment (unwrap! (map-get? circle-commitments-v2 { user: user, circle-id: circle-id })
                           ERR_COMMITMENT_NOT_FOUND))
      (commitment-usd (get commitment-usd commitment))
      (committed-data (get-user-committed user))
      (current-committed (get total-committed-usd committed-data))
      (new-committed (if (> current-committed commitment-usd)
                        (- current-committed commitment-usd)
                        u0))
    )
      (map-set user-committed user { total-committed-usd: new-committed })
      (map-delete circle-commitments-v2 { user: user, circle-id: circle-id })

      (print {
        event: "collateral-released-v2",
        user: user,
        circle-id: circle-id,
        released-usd: commitment-usd,
        remaining-committed: new-committed
      })

      (ok true)
    )
  )
)

;; Slash collateral on default
;; Slashes from deposits in priority order: hUSD first, then STX, then sBTC
;; slash-usd is in 6-decimal USD terms
(define-public (slash-collateral (user principal) (circle-id uint) (slash-usd uint))
  (let (
    (caller contract-caller)
  )
    (asserts! (is-authorized-caller caller) ERR_NOT_AUTHORIZED)
    (asserts! (> slash-usd u0) ERR_INVALID_AMOUNT)

    ;; Release the circle commitment first
    (let (
      (commitment (unwrap! (map-get? circle-commitments-v2 { user: user, circle-id: circle-id })
                           ERR_COMMITMENT_NOT_FOUND))
      (commitment-usd (get commitment-usd commitment))
      (committed-data (get-user-committed user))
      (current-committed (get total-committed-usd committed-data))
      (new-committed (if (> current-committed commitment-usd)
                        (- current-committed commitment-usd)
                        u0))
    )
      ;; Update committed
      (map-set user-committed user { total-committed-usd: new-committed })
      (map-delete circle-commitments-v2 { user: user, circle-id: circle-id })

      ;; Slash from hUSD first
      (let (
        (remaining-slash slash-usd)
        (slash-result-husd (slash-from-asset user ASSET_TYPE_HUSD remaining-slash))
        (remaining-after-husd (get remaining slash-result-husd))
        ;; Then STX
        (slash-result-stx (slash-from-asset user ASSET_TYPE_STX remaining-after-husd))
        (remaining-after-stx (get remaining slash-result-stx))
        ;; Then sBTC
        (slash-result-sbtc (slash-from-asset user ASSET_TYPE_SBTC remaining-after-stx))
        (final-remaining (get remaining slash-result-sbtc))
        (total-slashed (if (> slash-usd final-remaining) (- slash-usd final-remaining) u0))
      )
        (print {
          event: "collateral-slashed-v2",
          user: user,
          circle-id: circle-id,
          slash-usd: slash-usd,
          total-slashed-usd: total-slashed
        })

        (ok total-slashed)
      )
    )
  )
)

;; Private helper: slash USD amount from a specific asset
;; Returns { slashed: uint, remaining: uint } -- amounts in USD terms
(define-private (slash-from-asset (user principal) (asset-type uint) (slash-usd uint))
  (if (is-eq slash-usd u0)
    { slashed: u0, remaining: u0 }
    (match (map-get? user-asset-deposits { user: user, asset-type: asset-type })
      dep (match (map-get? supported-assets asset-type)
        asset (let (
          (deposited (get deposited dep))
          (price (get price-usd asset))
          (decimals (get decimals asset))
          ;; USD value of user's deposit
          (deposit-usd (/ (* deposited price) (pow u10 decimals)))
          ;; How much USD can we slash from this asset
          (slash-from-this (min-uint slash-usd deposit-usd))
          ;; Convert USD slash back to token micro-units
          (tokens-to-slash (if (> price u0)
            (/ (* slash-from-this (pow u10 decimals)) price)
            u0))
          (actual-tokens (min-uint tokens-to-slash deposited))
        )
          ;; Update yield before modifying
          (update-asset-reward asset-type)
          (update-user-asset-reward user asset-type)
          ;; Re-read deposit after yield update
          (let (
            (updated-dep (unwrap-panic (map-get? user-asset-deposits { user: user, asset-type: asset-type })))
            (new-deposited (if (> (get deposited updated-dep) actual-tokens)
              (- (get deposited updated-dep) actual-tokens) u0))
          )
            (map-set user-asset-deposits { user: user, asset-type: asset-type }
              (merge updated-dep { deposited: new-deposited })
            )
            ;; Update asset total
            (map-set supported-assets asset-type
              (merge asset {
                total-deposited: (if (> (get total-deposited asset) actual-tokens)
                  (- (get total-deposited asset) actual-tokens) u0)
              })
            )
            { slashed: slash-from-this, remaining: (if (> slash-usd slash-from-this) (- slash-usd slash-from-this) u0) }
          )
        )
        { slashed: u0, remaining: slash-usd } ;; asset config not found
      )
      { slashed: u0, remaining: slash-usd } ;; no deposit for this asset
    )
  )
)

;; ============================================
;; ADMIN FUNCTIONS
;; ============================================

;; Configure a supported asset
(define-public (configure-asset
  (asset-type uint)
  (token-principal (optional principal))
  (ltv-ratio uint)
  (decimals uint)
)
  (begin
    (asserts! (is-eq tx-sender (var-get admin)) ERR_NOT_AUTHORIZED)
    (asserts! (<= asset-type u2) ERR_INVALID_PARAMS)
    (asserts! (>= ltv-ratio u1000) ERR_INVALID_PARAMS) ;; min 10% LTV
    (asserts! (<= ltv-ratio u8000) ERR_INVALID_PARAMS) ;; max 80% LTV
    (asserts! (<= decimals u18) ERR_INVALID_PARAMS) ;; max 18 decimals

    (map-set supported-assets asset-type {
      token-principal: token-principal,
      ltv-ratio: ltv-ratio,
      price-usd: (match (map-get? supported-assets asset-type)
        existing (get price-usd existing)
        u0),
      decimals: decimals,
      price-last-updated: stacks-block-height,
      is-active: true,
      ;; Preserve existing yield state or initialize
      reward-per-token-stored: (match (map-get? supported-assets asset-type)
        existing (get reward-per-token-stored existing)
        u0),
      last-update-block: stacks-block-height,
      reward-rate: (match (map-get? supported-assets asset-type)
        existing (get reward-rate existing)
        u0),
      reward-end-block: (match (map-get? supported-assets asset-type)
        existing (get reward-end-block existing)
        u0),
      total-deposited: (match (map-get? supported-assets asset-type)
        existing (get total-deposited existing)
        u0)
    })

    (print { event: "asset-configured", asset-type: asset-type, ltv-ratio: ltv-ratio, decimals: decimals })
    (ok true)
  )
)

;; Set asset price (oracle update)
(define-public (set-asset-price (asset-type uint) (price-usd uint))
  (begin
    (asserts! (is-eq tx-sender (var-get admin)) ERR_NOT_AUTHORIZED)
    (asserts! (> price-usd u0) ERR_ZERO_PRICE)

    (let (
      (asset (unwrap! (map-get? supported-assets asset-type) ERR_ASSET_NOT_FOUND))
    )
      (map-set supported-assets asset-type
        (merge asset {
          price-usd: price-usd,
          price-last-updated: stacks-block-height
        })
      )
      (print { event: "price-updated-v2", asset-type: asset-type, price-usd: price-usd })
      (ok true)
    )
  )
)

;; Fund yield pool for a specific asset
(define-public (fund-yield-husd (token <ft-trait>) (amount uint) (duration-blocks uint))
  (let (
    (asset (unwrap! (map-get? supported-assets ASSET_TYPE_HUSD) ERR_ASSET_NOT_FOUND))
    (expected-token (unwrap! (get token-principal asset) ERR_TOKEN_NOT_SUPPORTED))
  )
    (asserts! (is-eq tx-sender (var-get admin)) ERR_NOT_AUTHORIZED)
    (asserts! (is-eq (contract-of token) expected-token) ERR_TOKEN_NOT_SUPPORTED)
    (asserts! (> amount u0) ERR_INVALID_AMOUNT)
    (asserts! (>= duration-blocks MIN_YIELD_DURATION) ERR_INVALID_PARAMS)

    (update-asset-reward ASSET_TYPE_HUSD)
    (try! (contract-call? token transfer amount tx-sender (as-contract tx-sender) none))

    (let (
      (current-block stacks-block-height)
      (updated-asset (unwrap-panic (map-get? supported-assets ASSET_TYPE_HUSD)))
      (remaining (if (> (get reward-end-block updated-asset) current-block)
        (* (get reward-rate updated-asset) (- (get reward-end-block updated-asset) current-block))
        u0))
      (total-reward (+ amount remaining))
      (new-rate (/ total-reward duration-blocks))
    )
      (map-set supported-assets ASSET_TYPE_HUSD
        (merge updated-asset {
          reward-rate: new-rate,
          reward-end-block: (+ current-block duration-blocks),
          last-update-block: current-block
        })
      )
      (print { event: "yield-funded-v2", asset-type: ASSET_TYPE_HUSD, amount: amount, rate: new-rate })
      (ok true)
    )
  )
)

;; Fund STX yield pool (admin sends STX)
(define-public (fund-yield-stx (amount uint) (duration-blocks uint))
  (begin
    (asserts! (is-eq tx-sender (var-get admin)) ERR_NOT_AUTHORIZED)
    (asserts! (> amount u0) ERR_INVALID_AMOUNT)
    (asserts! (>= duration-blocks MIN_YIELD_DURATION) ERR_INVALID_PARAMS)

    (update-asset-reward ASSET_TYPE_STX)
    (try! (stx-transfer? amount tx-sender (as-contract tx-sender)))

    (let (
      (current-block stacks-block-height)
      (asset (unwrap-panic (map-get? supported-assets ASSET_TYPE_STX)))
      (remaining (if (> (get reward-end-block asset) current-block)
        (* (get reward-rate asset) (- (get reward-end-block asset) current-block))
        u0))
      (total-reward (+ amount remaining))
      (new-rate (/ total-reward duration-blocks))
    )
      (map-set supported-assets ASSET_TYPE_STX
        (merge asset {
          reward-rate: new-rate,
          reward-end-block: (+ current-block duration-blocks),
          last-update-block: current-block
        })
      )
      (print { event: "yield-funded-v2", asset-type: ASSET_TYPE_STX, amount: amount, rate: new-rate })
      (ok true)
    )
  )
)

;; Fund sBTC yield pool
(define-public (fund-yield-sbtc (token <ft-trait>) (amount uint) (duration-blocks uint))
  (let (
    (asset (unwrap! (map-get? supported-assets ASSET_TYPE_SBTC) ERR_ASSET_NOT_FOUND))
    (expected-token (unwrap! (get token-principal asset) ERR_TOKEN_NOT_SUPPORTED))
  )
    (asserts! (is-eq tx-sender (var-get admin)) ERR_NOT_AUTHORIZED)
    (asserts! (is-eq (contract-of token) expected-token) ERR_TOKEN_NOT_SUPPORTED)
    (asserts! (> amount u0) ERR_INVALID_AMOUNT)
    (asserts! (>= duration-blocks MIN_YIELD_DURATION) ERR_INVALID_PARAMS)

    (update-asset-reward ASSET_TYPE_SBTC)
    (try! (contract-call? token transfer amount tx-sender (as-contract tx-sender) none))

    (let (
      (current-block stacks-block-height)
      (updated-asset (unwrap-panic (map-get? supported-assets ASSET_TYPE_SBTC)))
      (remaining (if (> (get reward-end-block updated-asset) current-block)
        (* (get reward-rate updated-asset) (- (get reward-end-block updated-asset) current-block))
        u0))
      (total-reward (+ amount remaining))
      (new-rate (/ total-reward duration-blocks))
    )
      (map-set supported-assets ASSET_TYPE_SBTC
        (merge updated-asset {
          reward-rate: new-rate,
          reward-end-block: (+ current-block duration-blocks),
          last-update-block: current-block
        })
      )
      (print { event: "yield-funded-v2", asset-type: ASSET_TYPE_SBTC, amount: amount, rate: new-rate })
      (ok true)
    )
  )
)

;; Authorize a contract to lock/release/slash collateral
(define-public (authorize-contract (contract principal))
  (begin
    (asserts! (is-eq tx-sender (var-get admin)) ERR_NOT_AUTHORIZED)
    (let (
      (current (var-get authorized-contracts))
    )
      (asserts! (is-none (index-of? current contract)) ERR_ALREADY_AUTHORIZED)
      (var-set authorized-contracts
        (unwrap! (as-max-len? (append current contract) u10) ERR_NOT_AUTHORIZED))
      (print { event: "contract-authorized-v2", contract: contract })
      (ok true)
    )
  )
)

;; Emergency pause -- blocks deposits, withdrawals, and yield claims
(define-public (pause-vault)
  (begin
    (asserts! (is-eq tx-sender (var-get admin)) ERR_NOT_AUTHORIZED)
    (var-set vault-paused true)
    (print { event: "vault-paused" })
    (ok true)
  )
)

;; Unpause vault
(define-public (unpause-vault)
  (begin
    (asserts! (is-eq tx-sender (var-get admin)) ERR_NOT_AUTHORIZED)
    (var-set vault-paused false)
    (print { event: "vault-unpaused" })
    (ok true)
  )
)

;; Check if vault is paused
(define-read-only (is-vault-paused)
  (var-get vault-paused)
)

;; Transfer admin role
(define-public (set-admin (new-admin principal))
  (begin
    (asserts! (is-eq tx-sender (var-get admin)) ERR_NOT_AUTHORIZED)
    (var-set admin new-admin)
    (print { event: "admin-transferred-v2", new-admin: new-admin })
    (ok true)
  )
)

Functions (42)

FunctionAccessArgs
min-uintprivatea: uint, b: uint
is-authorized-callerprivatecaller: principal
get-or-create-user-depositprivateuser: principal, asset-type: uint
get-asset-rptprivateasset-type: uint
update-asset-rewardprivateasset-type: uint
update-user-asset-rewardprivateuser: principal, asset-type: uint
calculate-asset-capacity-usdprivateasset-type: uint, deposited: uint
get-asset-configread-onlyasset-type: uint
get-user-depositread-onlyuser: principal, asset-type: uint
get-user-committedread-onlyuser: principal
get-circle-commitmentread-onlyuser: principal, circle-id: uint
get-total-capacityread-onlyuser: principal
get-available-capacityread-onlyuser: principal
can-commitread-onlyuser: principal, additional-usd: uint
get-pending-yieldread-onlyuser: principal, asset-type: uint
get-adminread-only
is-authorizedread-onlycaller: principal
get-vault-summaryread-onlyuser: principal
deposit-husdpublictoken: <ft-trait>, amount: uint
deposit-stxpublicamount: uint
deposit-sbtcpublictoken: <ft-trait>, amount: uint
check-withdrawal-allowedprivateuser: principal, asset-type: uint, amount: uint
withdraw-husdpublictoken: <ft-trait>, amount: uint
withdraw-stxpublicamount: uint
withdraw-sbtcpublictoken: <ft-trait>, amount: uint
claim-yield-husdpublictoken: <ft-trait>
claim-yield-stxpublic
claim-yield-sbtcpublictoken: <ft-trait>
lock-collateralpublicuser: principal, circle-id: uint, commitment-usd: uint
release-collateralpublicuser: principal, circle-id: uint
slash-collateralpublicuser: principal, circle-id: uint, slash-usd: uint
slash-from-assetprivateuser: principal, asset-type: uint, slash-usd: uint
configure-assetpublicasset-type: uint, token-principal: (optional principal
set-asset-pricepublicasset-type: uint, price-usd: uint
fund-yield-husdpublictoken: <ft-trait>, amount: uint, duration-blocks: uint
fund-yield-stxpublicamount: uint, duration-blocks: uint
fund-yield-sbtcpublictoken: <ft-trait>, amount: uint, duration-blocks: uint
authorize-contractpubliccontract: principal
pause-vaultpublic
unpause-vaultpublic
is-vault-pausedread-only
set-adminpublicnew-admin: principal