Source Code

;; ============================================================
;; P2P sBTC Lending Marketplace
;; Project: Stakvlt (Pivot)
;; Language: Clarity (Stacks Blockchain)
;; Description: A trustless peer-to-peer lending contract where
;;   lenders offer sBTC loans, borrowers lock collateral, and
;;   repayment/default is enforced automatically by the contract.
;; ============================================================

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

;; Contract owner (deployer)
(define-constant CONTRACT-OWNER tx-sender)

;; Error codes
(define-constant ERR-NOT-FOUND (err u100))
(define-constant ERR-ALREADY-EXISTS (err u101))
(define-constant ERR-INSUFFICIENT-COLLATERAL (err u102))
(define-constant ERR-LOAN-NOT-ACTIVE (err u103))
(define-constant ERR-NOT-AUTHORIZED (err u104))
(define-constant ERR-DEADLINE-NOT-PASSED (err u105))
(define-constant ERR-DEADLINE-PASSED (err u106))
(define-constant ERR-LOAN-ALREADY-TAKEN (err u107))
(define-constant ERR-TRANSFER-FAILED (err u108))

;; Collateral ratio: borrower must lock 150% of loan value
;; e.g. borrow 100 sats  lock 150 sats as collateral
(define-constant COLLATERAL-RATIO u150)

;; Loan duration in blocks (~30 days on Stacks = ~4320 blocks)
(define-constant LOAN-DURATION-BLOCKS u4320)

;; sBTC Token Contract (Stacks mainnet address)
(define-constant SBTC-TOKEN 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token)

;; ============================================================
;; DATA STRUCTURES
;; ============================================================

;; Loan offer created by a lender
(define-map loan-offers
  { loan-id: uint }
  {
    lender: principal, ;; wallet address of lender
    amount: uint, ;; loan amount in satoshis
    interest-rate: uint, ;; interest in basis points (e.g. 500 = 5%)
    collateral-required: uint, ;; collateral amount in satoshis (150% of loan)
    is-taken: bool, ;; has a borrower accepted this offer?
    is-active: bool, ;; is this offer still open?
  }
)

;; Active loan between lender and borrower
(define-map active-loans
  { loan-id: uint }
  {
    borrower: principal, ;; wallet address of borrower
    lender: principal, ;; wallet address of lender
    loan-amount: uint, ;; amount borrowed in satoshis
    collateral-amount: uint, ;; collateral locked in satoshis
    repayment-amount: uint, ;; total due (principal + interest) in satoshis
    start-block: uint, ;; block when loan started
    deadline-block: uint, ;; block when loan expires
    is-repaid: bool, ;; has borrower repaid?
    is-defaulted: bool, ;; has loan been defaulted/liquidated?
  }
)

;; Counter to generate unique loan IDs
(define-data-var loan-counter uint u0)

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

;; Calculate collateral required for a given loan amount
;; collateral = (loan-amount * 150) / 100
(define-private (calculate-collateral (loan-amount uint))
  (/ (* loan-amount COLLATERAL-RATIO) u100)
)

;; Calculate total repayment amount (principal + interest)
;; interest-rate is in basis points: 500 = 5%
;; repayment = loan-amount + (loan-amount * interest-rate / 10000)
(define-private (calculate-repayment
    (loan-amount uint)
    (interest-rate uint)
  )
  (+ loan-amount (/ (* loan-amount interest-rate) u10000))
)

;; Get next loan ID and increment counter
(define-private (get-next-loan-id)
  (let ((current-id (var-get loan-counter)))
    (var-set loan-counter (+ current-id u1))
    current-id
  )
)

;; ============================================================
;; PUBLIC FUNCTIONS
;; ============================================================

;; ------------------------------------------------------------
;; STEP 1: Lender creates a loan offer
;; Lender deposits sBTC into the contract to make it available
;; ------------------------------------------------------------
(define-public (create-loan-offer
    (amount uint)
    (interest-rate uint)
  )
  (let (
      (loan-id (get-next-loan-id))
      (collateral-needed (calculate-collateral amount))
    )
    ;; Transfer lender's sBTC into the contract escrow
    (try! (contract-call? SBTC-TOKEN transfer amount tx-sender (as-contract tx-sender)
      none
    ))

    ;; Store the loan offer
    (map-set loan-offers { loan-id: loan-id } {
      lender: tx-sender,
      amount: amount,
      interest-rate: interest-rate,
      collateral-required: collateral-needed,
      is-taken: false,
      is-active: true,
    })

    ;; Return the loan ID so the lender knows their offer ID
    (ok loan-id)
  )
)

;; ------------------------------------------------------------
;; STEP 2: Borrower accepts a loan offer
;; Borrower deposits collateral  receives the loan amount
;; ------------------------------------------------------------
(define-public (accept-loan (loan-id uint))
  (let (
      (offer (unwrap! (map-get? loan-offers { loan-id: loan-id }) ERR-NOT-FOUND))
      (loan-amount (get amount offer))
      (collateral-needed (get collateral-required offer))
      (repayment-due (calculate-repayment loan-amount (get interest-rate offer)))
      (deadline (+ block-height LOAN-DURATION-BLOCKS))
    )
    ;; Check offer is still open
    (asserts! (get is-active offer) ERR-LOAN-NOT-ACTIVE)
    (asserts! (not (get is-taken offer)) ERR-LOAN-ALREADY-TAKEN)

    ;; Borrower deposits collateral into contract
    (try! (contract-call? SBTC-TOKEN transfer collateral-needed tx-sender
      (as-contract tx-sender) none
    ))

    ;; Contract sends loan amount to borrower
    (try! (as-contract (contract-call? SBTC-TOKEN transfer loan-amount tx-sender tx-sender none)))

    ;; Mark offer as taken
    (map-set loan-offers { loan-id: loan-id }
      (merge offer {
        is-taken: true,
        is-active: false,
      })
    )

    ;; Create active loan record
    (map-set active-loans { loan-id: loan-id } {
      borrower: tx-sender,
      lender: (get lender offer),
      loan-amount: loan-amount,
      collateral-amount: collateral-needed,
      repayment-amount: repayment-due,
      start-block: block-height,
      deadline-block: deadline,
      is-repaid: false,
      is-defaulted: false,
    })

    (ok loan-id)
  )
)

;; ------------------------------------------------------------
;; STEP 3: Borrower repays the loan before deadline
;; Borrower sends repayment  gets collateral back
;; ------------------------------------------------------------
(define-public (repay-loan (loan-id uint))
  (let (
      (loan (unwrap! (map-get? active-loans { loan-id: loan-id }) ERR-NOT-FOUND))
      (repayment-amount (get repayment-amount loan))
      (collateral-amount (get collateral-amount loan))
      (lender (get lender loan))
    )
    ;; Only the borrower can repay
    (asserts! (is-eq tx-sender (get borrower loan)) ERR-NOT-AUTHORIZED)

    ;; Loan must not already be repaid or defaulted
    (asserts! (not (get is-repaid loan)) ERR-LOAN-NOT-ACTIVE)
    (asserts! (not (get is-defaulted loan)) ERR-LOAN-NOT-ACTIVE)

    ;; Must repay before deadline
    (asserts! (<= block-height (get deadline-block loan)) ERR-DEADLINE-PASSED)

    ;; Borrower sends repayment to contract
    (try! (contract-call? SBTC-TOKEN transfer repayment-amount tx-sender
      (as-contract tx-sender) none
    ))

    ;; Contract forwards repayment to lender
    (try! (as-contract (contract-call? SBTC-TOKEN transfer repayment-amount tx-sender lender none)))

    ;; Contract returns collateral to borrower
    (try! (as-contract (contract-call? SBTC-TOKEN transfer collateral-amount tx-sender
      (get borrower loan) none
    )))

    ;; Mark loan as repaid
    (map-set active-loans { loan-id: loan-id } (merge loan { is-repaid: true }))

    (ok true)
  )
)

;; ------------------------------------------------------------
;; STEP 4: Liquidate a defaulted loan (anyone can trigger)
;; If deadline passed and no repayment  lender gets collateral
;; ------------------------------------------------------------
(define-public (liquidate-loan (loan-id uint))
  (let (
      (loan (unwrap! (map-get? active-loans { loan-id: loan-id }) ERR-NOT-FOUND))
      (collateral-amount (get collateral-amount loan))
      (lender (get lender loan))
    )
    ;; Loan must not already be settled
    (asserts! (not (get is-repaid loan)) ERR-LOAN-NOT-ACTIVE)
    (asserts! (not (get is-defaulted loan)) ERR-LOAN-NOT-ACTIVE)

    ;; Deadline must have passed
    (asserts! (> block-height (get deadline-block loan)) ERR-DEADLINE-NOT-PASSED)

    ;; Contract sends collateral to lender as compensation
    (try! (as-contract (contract-call? SBTC-TOKEN transfer collateral-amount tx-sender lender none)))

    ;; Mark loan as defaulted
    (map-set active-loans { loan-id: loan-id }
      (merge loan { is-defaulted: true })
    )

    (ok true)
  )
)

;; ------------------------------------------------------------
;; STEP 5: Lender cancels an untaken offer and gets sBTC back
;; ------------------------------------------------------------
(define-public (cancel-offer (loan-id uint))
  (let ((offer (unwrap! (map-get? loan-offers { loan-id: loan-id }) ERR-NOT-FOUND)))
    ;; Only the lender can cancel
    (asserts! (is-eq tx-sender (get lender offer)) ERR-NOT-AUTHORIZED)

    ;; Can only cancel if not yet taken
    (asserts! (not (get is-taken offer)) ERR-LOAN-ALREADY-TAKEN)
    (asserts! (get is-active offer) ERR-LOAN-NOT-ACTIVE)

    ;; Return lender's sBTC
    (try! (as-contract (contract-call? SBTC-TOKEN transfer (get amount offer) tx-sender
      (get lender offer) none
    )))

    ;; Deactivate the offer
    (map-set loan-offers { loan-id: loan-id } (merge offer { is-active: false }))

    (ok true)
  )
)

;; ============================================================
;; READ-ONLY FUNCTIONS (for the frontend UI)
;; ============================================================

;; Get details of a loan offer
(define-read-only (get-loan-offer (loan-id uint))
  (map-get? loan-offers { loan-id: loan-id })
)

;; Get details of an active loan
(define-read-only (get-active-loan (loan-id uint))
  (map-get? active-loans { loan-id: loan-id })
)

;; Get total number of loans created
(define-read-only (get-loan-count)
  (var-get loan-counter)
)

;; Check if a loan is past its deadline
(define-read-only (is-loan-overdue (loan-id uint))
  (match (map-get? active-loans { loan-id: loan-id })
    loan (> block-height (get deadline-block loan))
    false
  )
)

;; Calculate what a borrower owes for a given loan amount and rate
(define-read-only (preview-repayment
    (loan-amount uint)
    (interest-rate uint)
  )
  (calculate-repayment loan-amount interest-rate)
)

;; Calculate collateral needed for a given loan amount
(define-read-only (preview-collateral (loan-amount uint))
  (calculate-collateral loan-amount)
)

Functions (11)

FunctionAccessArgs
calculate-collateralprivateloan-amount: uint
get-next-loan-idprivate
accept-loanpublicloan-id: uint
repay-loanpublicloan-id: uint
liquidate-loanpublicloan-id: uint
cancel-offerpublicloan-id: uint
get-loan-offerread-onlyloan-id: uint
get-active-loanread-onlyloan-id: uint
get-loan-countread-only
is-loan-overdueread-onlyloan-id: uint
preview-collateralread-onlyloan-amount: uint