Source Code

;; Dynamic NFT-Backed Loan Smart Contract
;; Manages loans backed by NFTs that change attributes based on repayment status

;; Constants
(define-constant CONTRACT_OWNER tx-sender)
(define-constant ERR_NOT_AUTHORIZED (err u100))
(define-constant ERR_NFT_NOT_FOUND (err u101))
(define-constant ERR_ALREADY_LISTED (err u102))
(define-constant ERR_NOT_LISTED (err u103))
(define-constant ERR_INSUFFICIENT_VALUE (err u104))
(define-constant ERR_LOAN_NOT_FOUND (err u105))
(define-constant ERR_LOAN_DEFAULTED (err u106))
(define-constant ERR_LOAN_NOT_DUE (err u107))
(define-constant ERR_LOAN_CLOSED (err u108))

;; NFT Definition
(define-non-fungible-token dynamic-nft uint)

;; Data Maps
(define-map token-attributes
    { token-id: uint }
    {
        rarity: uint,
        power-level: uint,
        condition: uint,
        last-updated: uint,
        last-updated-time: uint
    }
)

(define-map loan-details
    { loan-id: uint }
    {
        borrower: principal,
        lender: principal,
        token-id: uint,
        amount: uint,
        interest-rate: uint,
        duration: uint,
        start-block: uint,
        start-time: uint,
        status: (string-ascii 20),
        missed-payments: uint,
        total-repaid: uint
    }
)

(define-map token-loans 
    { token-id: uint }
    { loan-id: uint }
)

(define-map loan-listings
    { token-id: uint }
    {
        owner: principal,
        requested-amount: uint,
        min-duration: uint,
        max-interest: uint
    }
)

;; Variables
(define-data-var next-token-id uint u1)
(define-data-var next-loan-id uint u1)

;; Read-only functions
(define-read-only (get-token-attributes (token-id uint))
    (map-get? token-attributes { token-id: token-id })
)

(define-read-only (get-loan-details (loan-id uint))
    (map-get? loan-details { loan-id: loan-id })
)

(define-read-only (get-token-loan (token-id uint))
    (map-get? token-loans { token-id: token-id })
)

(define-read-only (get-loan-listing (token-id uint))
    (map-get? loan-listings { token-id: token-id })
)

;; Clarity v4 helper functions using to-ascii?
(define-read-only (get-loan-status-ascii (loan-id uint))
    (match (get-loan-details loan-id)
        loan-data (ok (get status loan-data))
        ERR_LOAN_NOT_FOUND
    )
)

;; (define-read-only (get-borrower-ascii (loan-id uint))
;;     (match (get-loan-details loan-id)
;;         loan-data (to-ascii? (get borrower loan-data))
;;         none
;;     )
;; )

;; (define-read-only (get-lender-ascii (loan-id uint))
;;     (match (get-loan-details loan-id)
;;         loan-data (to-ascii? (get lender loan-data))
;;         none
;;     )
;; )

;; Get loan timestamp (Clarity v4 feature)
(define-read-only (get-loan-start-time (loan-id uint))
    (match (get-loan-details loan-id)
        loan-data (some (get start-time loan-data))
        none
    )
)

;; Mint new NFT
(define-public (mint-nft (recipient principal))
    (let 
        ((token-id (var-get next-token-id)))

        ;; Mint NFT
        (try! (nft-mint? dynamic-nft token-id recipient))

        ;; Set initial attributes
        (map-set token-attributes
            { token-id: token-id }
            {
                rarity: u100,
                power-level: u100,
                condition: u100,
                last-updated: stacks-block-time,
                last-updated-time: stacks-block-time
            }
        )

        ;; Increment token ID
        (var-set next-token-id (+ token-id u1))
        (ok token-id)
    )
)

;; List NFT for loan
(define-public (list-nft-for-loan 
    (token-id uint) 
    (requested-amount uint)
    (min-duration uint)
    (max-interest uint))

    (let ((owner (unwrap! (nft-get-owner? dynamic-nft token-id) ERR_NFT_NOT_FOUND)))
        ;; Checks
        (asserts! (is-eq tx-sender owner) ERR_NOT_AUTHORIZED)
        (asserts! (is-none (get-loan-listing token-id)) ERR_ALREADY_LISTED)

        ;; Create listing
        (map-set loan-listings
            { token-id: token-id }
            {
                owner: tx-sender,
                requested-amount: requested-amount,
                min-duration: min-duration,
                max-interest: max-interest
            }
        )
        (ok true)
    )
)

;; Offer loan
(define-public (offer-loan 
    (token-id uint)
    (amount uint)
    (interest-rate uint)
    (duration uint))

    (let 
        ((listing (unwrap! (get-loan-listing token-id) ERR_NOT_LISTED))
         (loan-id (var-get next-loan-id)))

        ;; Checks
        (asserts! (>= amount (get requested-amount listing)) ERR_INSUFFICIENT_VALUE)
        (asserts! (>= duration (get min-duration listing)) ERR_INSUFFICIENT_VALUE)
        (asserts! (<= interest-rate (get max-interest listing)) ERR_INSUFFICIENT_VALUE)

        ;; Transfer STX to borrower
        (try! (stx-transfer? amount tx-sender (get owner listing)))

        ;; Create loan
        (map-set loan-details
            { loan-id: loan-id }
            {
                borrower: (get owner listing),
                lender: tx-sender,
                token-id: token-id,
                amount: amount,
                interest-rate: interest-rate,
                duration: duration,
                start-block: stacks-block-time,
                start-time: stacks-block-time,
                status: "active",
                missed-payments: u0,
                total-repaid: u0
            }
        )

        ;; Link token to loan
        (map-set token-loans { token-id: token-id } { loan-id: loan-id })

        ;; Remove listing
        (map-delete loan-listings { token-id: token-id })

        ;; Transfer NFT to lender as collateral (Clarity v4 pattern)
        (try! (nft-transfer? dynamic-nft token-id (get owner listing) tx-sender))

        ;; Increment loan ID
        (var-set next-loan-id (+ loan-id u1))
        (ok loan-id)
    )
)

;; Internal function to calculate payment
(define-private (calculate-payment (loan (tuple (amount uint) (interest-rate uint) (duration uint) (start-block uint))))
    (let
        ((total-amount (* (get amount loan) (+ u100 (get interest-rate loan))))
         (payment-per-block (/ total-amount (get duration loan))))
        payment-per-block
    )
)

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

(define-private (max-uint (a uint) (b uint))
    (if (>= a b) a b)
)

;; Update NFT attributes
(define-private (update-nft-attributes (loan-id uint) (payment uint) (payment-due uint))
    (match (get-loan-details loan-id)
        loan-data (match (get-token-attributes (get token-id loan-data))
            token-data (begin
                (map-set token-attributes
                    { token-id: (get token-id loan-data) }
                    (merge token-data {
                        condition: (if (>= payment payment-due)
                            (min-uint u100 (+ (get condition token-data) u5))
                            (max-uint u1 (- (get condition token-data) u10))),
                        power-level: (if (>= payment payment-due)
                            (min-uint u100 (+ (get power-level token-data) u3))
                            (max-uint u1 (- (get power-level token-data) u7))),
                        last-updated: stacks-block-time,
                        last-updated-time: stacks-block-time
                    })
                )
                (ok true))
            ERR_NFT_NOT_FOUND)
        ERR_LOAN_NOT_FOUND)
)


;; Close loan
(define-public (close-loan (loan-id uint))
    (let
        ((loan (unwrap! (get-loan-details loan-id) ERR_LOAN_NOT_FOUND)))

        ;; Checks
        (asserts! (is-eq (get status loan) "active") ERR_LOAN_CLOSED)
        (asserts! (>= (- stacks-block-time (get start-block loan)) (get duration loan)) ERR_LOAN_NOT_DUE)

        ;; Verify caller is the lender (they hold the NFT)
        (asserts! (is-eq tx-sender (get lender loan)) ERR_NOT_AUTHORIZED)

        (if (>= (get total-repaid loan) (get amount loan))
            (begin
                ;; Lender returns NFT to borrower (Clarity v4 pattern)
                (try! (nft-transfer?
                    dynamic-nft
                    (get token-id loan)
                    tx-sender
                    (get borrower loan)))

                ;; Update loan status
                (map-set loan-details
                    { loan-id: loan-id }
                    (merge loan { status: "completed" }))
                (ok true))

            (begin
                ;; Loan defaulted - lender keeps NFT, just update status
                ;; Update loan status
                (map-set loan-details
                    { loan-id: loan-id }
                    (merge loan { status: "defaulted" }))
                (ok false))
        )
    )
)

Functions (16)

FunctionAccessArgs
get-token-attributesread-onlytoken-id: uint
get-loan-detailsread-onlyloan-id: uint
get-token-loanread-onlytoken-id: uint
get-loan-listingread-onlytoken-id: uint
get-loan-status-asciiread-onlyloan-id: uint
get-borrower-asciiread-onlyloan-id: uint
get-lender-asciiread-onlyloan-id: uint
get-loan-start-timeread-onlyloan-id: uint
mint-nftpublicrecipient: principal
list-nft-for-loanpublictoken-id: uint, requested-amount: uint, min-duration: uint, max-interest: uint
offer-loanpublictoken-id: uint, amount: uint, interest-rate: uint, duration: uint
calculate-paymentprivateloan: (tuple (amount uint, interest-rate: uint, duration: uint, start-block: uint
min-uintprivatea: uint, b: uint
max-uintprivatea: uint, b: uint
update-nft-attributesprivateloan-id: uint, payment: uint, payment-due: uint
close-loanpublicloan-id: uint