Source Code

;; SPDX-License-Identifier: BUSL-1.1

;; TRAITS
(use-trait token-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait)

;; ERRORS
(define-constant ERR-INTEREST-PARAMS (err u20000))
(define-constant ERR-INSUFFICIENT-FREE-LIQUIDITY (err u20001))
(define-constant ERR-MAX-LTV (err u20002))
(define-constant ERR-NO-POSITION (err u20003))
(define-constant ERR-NOT-ENOUGH-SHARES (err u20004))
(define-constant ERR-LIST-OVERFLOW (err u20005))
(define-constant ERR-INSUFFICIENT-BALANCE (err u20006))
(define-constant ERR-COLLATERAL-NOT-SUPPORTED (err u20007))
(define-constant ERR-MISSING-MARKET-PRICE (err u20008))
(define-constant ERR-NO-DEBT (err u20009))
(define-constant ERR-NOT-TX-SENDER (err u20010))

;; CONSTANTS
(define-constant SUCCESS (ok true))
(define-constant SCALING-FACTOR (contract-call? .constants-v2 get-scaling-factor))
(define-constant MARKET-TOKEN-DECIMALS (contract-call? .constants-v2 get-market-token-decimals))
(define-constant PRICE-SCALING-FACTOR (contract-call? .constants-v2 get-price-scaling-factor))

;; PUBLIC FUNCTIONS
(define-public (borrow (pyth-price-feed-data (optional (buff 8192))) (amount uint) (maybe-user (optional principal)))
  (begin
    (try! (contract-call? .withdrawal-caps-v1 check-withdrawal-debt-cap amount))
    (try! (contract-call? .pyth-adapter-v1 update-pyth pyth-price-feed-data))
    (try! (accrue-interest))
    (asserts! (>= (contract-call? .state-v1 get-borrowable-balance) amount) ERR-INSUFFICIENT-FREE-LIQUIDITY)
    (let
      (
        (user (match maybe-user user (begin (asserts! (is-eq user tx-sender) ERR-NOT-TX-SENDER) user) contract-caller))
        ;; can't borrow if no collaterals were posted
        (borrow-params (contract-call? .state-v1 get-borrow-repay-params user))
        (position (unwrap! (get user-position borrow-params) ERR-NO-POSITION))
        (current-debt-shares (get debt-shares position))
        (debt-params (contract-call? .state-v1 get-debt-params))
        (current-debt (contract-call? .math-v1 convert-to-debt-assets debt-params current-debt-shares true))
        (new-debt-shares (contract-call? .math-v1 convert-to-debt-shares debt-params amount true))
        (total-user-debt-shares (+ new-debt-shares (get debt-shares position)))
        (position-collaterals (get collaterals position))
        (collateral-prices (try! (contract-call? .pyth-adapter-v1 bulk-read-collateral-prices position-collaterals)))
        (user-list (unwrap-panic (slice? (list user user user user user user user user user user) u0 (len collateral-prices))))
        (total-max-ltv (fold + (map iterate-collateral-value position-collaterals collateral-prices user-list) u0))
        (new-current-debt (+ amount current-debt))
        (market-asset-price (unwrap! (contract-call? .pyth-adapter-v1 read-price 'SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE.usdcx) ERR-MISSING-MARKET-PRICE))
        (new-current-debt-adjusted (contract-call? .math-v1 get-market-asset-value market-asset-price new-current-debt))
      )
      (asserts! (<= new-current-debt-adjusted total-max-ltv) ERR-MAX-LTV)
      (try! (contract-call? .state-v1 update-borrow-state {
        user: user,
        user-debt-shares: total-user-debt-shares,
        user-collaterals: position-collaterals,
        user-borrowed-amount: (+ (get borrowed-amount position) amount),
        shares: new-debt-shares,
        amount: amount,
        total-borrowed-amount: (+ (get total-borrowed-amount borrow-params) amount)
      }))
      (print {
        assets: amount,
        total-user-debt-shares: total-user-debt-shares,
        new-debt-shares: new-debt-shares,
        user: user,
        action: "borrow"
      })
      SUCCESS
    )
  )
)

(define-public (repay (amount uint) (on-behalf-of (optional principal)))
  (begin
    (try! (accrue-interest))
    (let
      (
        (user (default-to contract-caller on-behalf-of))
        (repay-params (contract-call? .state-v1 get-borrow-repay-params user))
        (position (unwrap! (get user-position repay-params) ERR-NO-POSITION))
        (total-borrowed-amount (get total-borrowed-amount repay-params))
        (interest-params (contract-call? .state-v1 get-open-interest))
        (open-interest (+ (get lp-open-interest interest-params) (get staked-open-interest interest-params) (get protocol-open-interest interest-params)))
        (repay-info (max-repay-amount amount (get debt-shares position)))
        (shares (get shares repay-info))
        (repay-amount (get repay-amount repay-info))
        (borrowed-amount (get borrowed-amount position))
        (current-debt (get current-debt repay-info))
        (debt-check (asserts! (> current-debt u0) ERR-NO-DEBT))
        (interest-portion (contract-call? .math-v1 calculate-interest-portions current-debt borrowed-amount repay-amount))
        (principal-part (get principal-part interest-portion))
        (interest-part (get interest-part interest-portion))
        (open-interest-without-principal (- open-interest total-borrowed-amount))
        (lp-open-interest-without-principal (- (get lp-open-interest interest-params) total-borrowed-amount))
        (lp-part (contract-call? .math-v1 safe-div (* interest-part lp-open-interest-without-principal) open-interest-without-principal))
        (protocol-part (contract-call? .math-v1 safe-div (* interest-part (get protocol-open-interest interest-params)) open-interest-without-principal))
        (staked-part (contract-call? .math-v1 safe-div (* interest-part (get staked-open-interest interest-params)) open-interest-without-principal))
        (asset-params (contract-call? .state-v1 get-lp-params))
        (staked-lp-tokens (contract-call? .math-v1 convert-to-shares asset-params staked-part false))
        (total-user-debt-shares (unwrap! (contract-call? .math-v1 sub (get debt-shares position) shares) ERR-NOT-ENOUGH-SHARES))
        (updated-borrowed-amount (contract-call? .math-v1 safe-sub borrowed-amount principal-part))
        (updated-total-borrowed-amount (contract-call? .math-v1 safe-sub total-borrowed-amount principal-part))
      )
      (asserts! (<= shares (get debt-shares position)) ERR-NOT-ENOUGH-SHARES)
      (try! (contract-call? .withdrawal-caps-v1 repay repay-amount))
      (try! (contract-call? .state-v1 update-repay-state {
        user: user,
        user-debt-shares: total-user-debt-shares,
        user-collaterals: (get collaterals position),
        shares: shares,
        amount: repay-amount,
        lp-part: (+ principal-part lp-part),
        protocol-part: protocol-part,
        staked-part: staked-part,
        staked-lp-tokens: staked-lp-tokens,
        payor: contract-caller,
        borrowed-amount: updated-borrowed-amount,
        total-borrowed-amount: updated-total-borrowed-amount,
        staking-contract: .staking-v1,
        borrowed-block: (get borrowed-block position)
      }))
      (try! (contract-call? .staking-v1 increase-lp-staked-balance staked-lp-tokens))
      (print {
        assets: repay-amount,
        total-user-debt-shares: total-user-debt-shares,
        repaid-debt-shares: shares,
        on-behalf-of: on-behalf-of,
        sender: contract-caller,
        action: "repay"
      })
      SUCCESS
    )
))

(define-public (add-collateral (collateral <token-trait>) (amount uint) (maybe-user (optional principal)))
  (begin
    (let
      (
        (user (match maybe-user user (begin (asserts! (is-eq user tx-sender) ERR-NOT-TX-SENDER) user) contract-caller))
        (collateral-token (contract-of collateral))
        (add-collateral-params (try! (contract-call? .state-v1 get-collateral-params collateral-token user)))
        (collateral-info (get collateral-info add-collateral-params))
        (user-balance (get user-balance add-collateral-params))
        (new-amount (+ amount (default-to u0 (get amount user-balance))))
        ;; if the user doesn't have a position, get a default one
        (position (get user-position add-collateral-params))
        ;; check if the collateral is already in the list, otherwise add it
        (updated-collaterals (if (is-none (index-of (get collaterals position) collateral-token))
          (unwrap! (add-item (get collaterals position) collateral-token) ERR-LIST-OVERFLOW)
          (get collaterals position)
        ))
      )
      (asserts! (> (get max-ltv collateral-info) u0) ERR-COLLATERAL-NOT-SUPPORTED)
      (try! (contract-call? .withdrawal-caps-v1 collateral-deposit collateral amount))
      (try! (contract-call? .state-v1 update-add-collateral collateral {
        amount: amount,
        total-collateral-amount: new-amount,
        user: user,
        user-position: {debt-shares: (get debt-shares position), collaterals: updated-collaterals, borrowed-amount: (get borrowed-amount position), borrowed-block: (get borrowed-block position)},
      }))
      (print {
        collateral: collateral-token,
        amount-deposited: amount,
        user-balance: new-amount,
        user: user,
        action: "add-collateral"
      })
      SUCCESS
    )
))

(define-public (remove-collateral (pyth-price-feed-data (optional (buff 8192))) (collateral <token-trait>) (amount uint) (maybe-user (optional principal)))
  (begin
    (try! (contract-call? .withdrawal-caps-v1 check-withdrawal-collateral-cap collateral amount))
    (try! (contract-call? .pyth-adapter-v1 update-pyth pyth-price-feed-data))
    (try! (accrue-interest))
    (let
      (
        (user (match maybe-user user (begin (asserts! (is-eq user tx-sender) ERR-NOT-TX-SENDER) user) contract-caller))
        (collateral-token (contract-of collateral))
        (remove-collateral-params (try! (contract-call? .state-v1 get-collateral-params collateral-token user)))
        (collateral-info (get collateral-info remove-collateral-params))
        (user-balance (unwrap! (get user-balance remove-collateral-params) ERR-INSUFFICIENT-BALANCE))
        (prev-amount (get amount user-balance))
        (position (get user-position remove-collateral-params))
        (remove-user-collateral-info (try! (remove-user-collateral user prev-amount amount collateral-token (get debt-shares position) (get collaterals position) (get borrowed-amount position) (get borrowed-block position))))
        (collateral-prices (try! (contract-call? .pyth-adapter-v1 bulk-read-collateral-prices (get position-collaterals remove-user-collateral-info))))
        (user-list (unwrap-panic (slice? (list user user user user user user user user user user) u0 (len collateral-prices))))
        (total-max-ltv (fold + (map iterate-collateral-value (get position-collaterals remove-user-collateral-info) collateral-prices user-list) u0))
        (debt-params (contract-call? .state-v1 get-debt-params))
        (current-debt (contract-call? .math-v1 convert-to-debt-assets debt-params (get debt-shares position) true))
        (market-asset-price (unwrap! (contract-call? .pyth-adapter-v1 read-price 'SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE.usdcx) ERR-MISSING-MARKET-PRICE))
        (current-debt-adjusted (contract-call? .math-v1 get-market-asset-value market-asset-price current-debt))
      )
      (asserts! (<= current-debt-adjusted total-max-ltv) ERR-MAX-LTV)

      ;; transfer the collateral tokens to the user
      (try! (contract-call? .state-v1 transfer-to collateral user amount))
      (print {
          collateral: collateral-token,
          amount-removed: amount,
          user-balance: (get remaining-amount remove-user-collateral-info),
          user: user,
          action: "remove-collateral"
      })
      SUCCESS
    )
))

;; READ-ONLY FUNCTIONS
(define-read-only (get-user-collaterals-value (account principal))
  (match (contract-call? .state-v1 get-user-position account) 
  position
  (let
    (
      (posted-collaterals (get collaterals position))
      (collateral-prices (try! (contract-call? .pyth-adapter-v1 bulk-read-collateral-prices posted-collaterals)))
    )
    (begin
      (ok (fold + (map get-collateral-value posted-collaterals (list
        account account account account account account account account account account
      ) collateral-prices) u0))
    )
  )
  ERR-NO-POSITION
))

;; PRIVATE FUNCTIONS

(define-private (accrue-interest)
  (let (
    (accrue-interest-params (unwrap! (contract-call? .state-v1 get-accrue-interest-params) ERR-INTEREST-PARAMS))
    (accrued-interest (try! (contract-call? .linear-kinked-ir-v1 accrue-interest
      (get last-accrued-block-time accrue-interest-params)
      (get lp-interest accrue-interest-params)
      (get staked-interest accrue-interest-params)
      (try! (contract-call? .staking-reward-v1 calculate-staking-reward-percentage (contract-call? .staking-v1 get-active-staked-lp-tokens)))
      (get protocol-interest accrue-interest-params)
      (get protocol-reserve-percentage accrue-interest-params)
      (get total-assets accrue-interest-params)))
    ))
    (contract-call? .state-v1 set-accrued-interest accrued-interest)
))


(define-private (get-collateral-value (collateral principal) (user principal) (collateral-price uint))
  (contract-call? .math-v1 to-fixed
    (/
      (* 
        (default-to u0 (get amount (contract-call? .state-v1 get-user-collateral user collateral)))
        collateral-price
      )
      PRICE-SCALING-FACTOR
    ) 
    (default-to u8 (get decimals (contract-call? .state-v1 get-collateral collateral)))
    MARKET-TOKEN-DECIMALS
  )
)

(define-private (iterate-collateral-value (collateral principal) (collateral-price uint) (user principal))
  (let
    (
      (collateral-info (contract-call? .state-v1 get-collateral collateral))
      (user-collateral-value (get-collateral-value collateral user collateral-price))
      (max-ltv (default-to u0 (get max-ltv collateral-info)))
    )
    (/ (* user-collateral-value max-ltv) SCALING-FACTOR)
))

(define-private (add-item (collaterals-list (list 10 principal)) (collateral principal))
  (as-max-len? (append collaterals-list collateral) u10)
)

(define-private (remove-user-collateral (user principal) (prev-amount uint) (amount uint) (collateral principal) (debt-shares uint) (position-collaterals (list 10 principal)) (borrowed-amount uint) (borrowed-block uint))
  (let ((remaining-amount (unwrap! (contract-call? .math-v1 sub prev-amount amount) ERR-INSUFFICIENT-BALANCE)))
    (if (is-eq remaining-amount u0)
      (let ((updated-position-collaterals (contract-call? .state-v1 remove-item position-collaterals collateral)))
        ;; remove the collateral since there is no user collateral left
        (try! (contract-call? .state-v1 update-remove-collateral user collateral debt-shares (get new-list updated-position-collaterals) borrowed-amount borrowed-block))
        (ok {
          remaining-amount: remaining-amount,
          position-collaterals: (get new-list updated-position-collaterals),
        })
      )
      (begin
        ;; decrease the amount of collateral deposited by the user
        (try! (contract-call? .state-v1 update-user-collateral user collateral remaining-amount))
        (ok {
          remaining-amount: remaining-amount,
          position-collaterals: position-collaterals,
        })
    ))
))

(define-private (max-repay-amount (amount uint) (total-user-debt-shares uint))
  (let (
      (debt-params (contract-call? .state-v1 get-debt-params))
      (current-debt (contract-call? .math-v1 convert-to-debt-assets debt-params total-user-debt-shares true))
      (repay-amount (if (>= amount current-debt) current-debt amount))
      (shares (if (is-eq repay-amount current-debt) total-user-debt-shares (contract-call? .math-v1 convert-to-debt-shares debt-params amount false)))
    )
    {
      current-debt: current-debt,
      repay-amount: repay-amount,
      shares: shares,
    }
))

Functions (11)

FunctionAccessArgs
borrowpublicpyth-price-feed-data: (optional (buff 8192
repaypublicamount: uint, on-behalf-of: (optional principal
add-collateralpubliccollateral: <token-trait>, amount: uint, maybe-user: (optional principal
remove-collateralpublicpyth-price-feed-data: (optional (buff 8192
get-user-collaterals-valueread-onlyaccount: principal
accrue-interestprivate
get-collateral-valueprivatecollateral: principal, user: principal, collateral-price: uint
iterate-collateral-valueprivatecollateral: principal, collateral-price: uint, user: principal
add-itemprivatecollaterals-list: (list 10 principal
remove-user-collateralprivateuser: principal, prev-amount: uint, amount: uint, collateral: principal, debt-shares: uint, position-collaterals: (list 10 principal
max-repay-amountprivateamount: uint, total-user-debt-shares: uint