Source Code

;; title: escrow-manager
;; version: 1.0.0
;; summary: Secure fund management beyond basic payments
;; description: Escrow accounts, milestone-based releases, and secure fund locking

;; constants
(define-constant contract-owner tx-sender)
(define-constant err-owner-only (err u100))
(define-constant err-not-found (err u101))
(define-constant err-unauthorized (err u102))
(define-constant err-insufficient-funds (err u103))
(define-constant err-invalid-status (err u104))
(define-constant err-milestone-incomplete (err u105))
(define-constant err-escrow-expired (err u106))
(define-constant err-escrow-active (err u107))
(define-constant err-invalid-amount (err u108))

;; Escrow status
(define-constant STATUS-ACTIVE u1)
(define-constant STATUS-RELEASED u2)
(define-constant STATUS-REFUNDED u3)
(define-constant STATUS-PARTIALLY-RELEASED u4)
(define-constant STATUS-EXPIRED u5)

;; Milestone status
(define-constant MILESTONE-PENDING u1)
(define-constant MILESTONE-COMPLETED u2)
(define-constant MILESTONE-FAILED u3)

;; data vars
(define-data-var escrow-nonce uint u0)
(define-data-var milestone-nonce uint u0)
(define-data-var min-escrow-amount uint u100000) ;; 0.1 STX
(define-data-var max-escrow-duration uint u52560) ;; ~1 year in blocks
(define-data-var emergency-withdrawal-fee uint u10) ;; 10% fee
(define-data-var total-escrowed uint u0)

;; data maps
(define-map escrow-accounts
    { escrow-id: uint }
    {
        campaign-id: uint,
        depositor: principal,
        beneficiary: principal,
        amount: uint,
        released-amount: uint,
        status: uint,
        created-at: uint,
        expiry: uint,
        requires-milestones: bool
    }
)

(define-map escrow-milestones
    { escrow-id: uint, milestone-id: uint }
    {
        description: (string-utf8 200),
        amount: uint,
        status: uint,
        condition-hash: (optional (buff 32)),
        deadline: uint,
        completed-at: uint
    }
)

(define-map milestone-count
    { escrow-id: uint }
    {
        total: uint,
        completed: uint
    }
)

(define-map escrow-releases
    { escrow-id: uint, release-id: uint }
    {
        amount: uint,
        recipient: principal,
        reason: (string-utf8 200),
        released-at: uint
    }
)

(define-map pending-escrows
    { user: principal }
    {
        total-locked: uint,
        total-escrowed-out: uint
    }
)

(define-map escrow-history
    { user: principal }
    {
        total-deposited: uint,
        total-received: uint,
        total-refunded: uint,
        escrows-completed: uint
    }
)

;; private functions
(define-private (calculate-emergency-fee (amount uint))
    (/ (* amount (var-get emergency-withdrawal-fee)) u100)
)

(define-private (update-user-pending (user principal) (amount uint) (is-deposit bool))
    (let
        (
            (pending (default-to
                { total-locked: u0, total-escrowed-out: u0 }
                (map-get? pending-escrows { user: user })
            ))
        )
        (map-set pending-escrows
            { user: user }
            (if is-deposit
                {
                    total-locked: (+ (get total-locked pending) amount),
                    total-escrowed-out: (get total-escrowed-out pending)
                }
                {
                    total-locked: (get total-locked pending),
                    total-escrowed-out: (+ (get total-escrowed-out pending) amount)
                }
            )
        )
    )
)

;; read only functions
(define-read-only (get-escrow (escrow-id uint))
    (map-get? escrow-accounts { escrow-id: escrow-id })
)

(define-read-only (get-milestone (escrow-id uint) (milestone-id uint))
    (map-get? escrow-milestones { escrow-id: escrow-id, milestone-id: milestone-id })
)

(define-read-only (get-milestone-count (escrow-id uint))
    (default-to { total: u0, completed: u0 } (map-get? milestone-count { escrow-id: escrow-id }))
)

(define-read-only (get-release (escrow-id uint) (release-id uint))
    (map-get? escrow-releases { escrow-id: escrow-id, release-id: release-id })
)

(define-read-only (get-pending-escrows (user principal))
    (map-get? pending-escrows { user: user })
)

(define-read-only (get-escrow-history (user principal))
    (map-get? escrow-history { user: user })
)

(define-read-only (get-escrow-nonce)
    (var-get escrow-nonce)
)

(define-read-only (is-escrow-expired (escrow-id uint))
    (match (map-get? escrow-accounts { escrow-id: escrow-id })
        escrow (>= stacks-block-time (get expiry escrow))
        false
    )
)

;; public functions
(define-public (create-escrow
    (campaign-id uint)
    (beneficiary principal)
    (amount uint)
    (duration uint)
    (requires-milestones bool)
)
    (let
        (
            (escrow-id (+ (var-get escrow-nonce) u1))
            (expiry (+ stacks-block-time duration))
        )
        (asserts! (>= amount (var-get min-escrow-amount)) err-invalid-amount)
        (asserts! (<= duration (var-get max-escrow-duration)) err-invalid-status)
        (asserts! (not (is-eq tx-sender beneficiary)) err-unauthorized)

        ;; In production, this would transfer STX to contract
        ;; (try! (stx-transfer? amount tx-sender (as-contract tx-sender)))

        (map-set escrow-accounts
            { escrow-id: escrow-id }
            {
                campaign-id: campaign-id,
                depositor: tx-sender,
                beneficiary: beneficiary,
                amount: amount,
                released-amount: u0,
                status: STATUS-ACTIVE,
                created-at: stacks-block-time,
                expiry: expiry,
                requires-milestones: requires-milestones
            }
        )

        (if requires-milestones
            (map-set milestone-count { escrow-id: escrow-id } { total: u0, completed: u0 })
            true
        )

        (update-user-pending tx-sender amount true)
        (var-set total-escrowed (+ (var-get total-escrowed) amount))
        (var-set escrow-nonce escrow-id)
        (ok escrow-id)
    )
)

(define-public (add-milestone
    (escrow-id uint)
    (description (string-utf8 200))
    (amount uint)
    (deadline uint)
    (condition-hash (optional (buff 32)))
)
    (let
        (
            (escrow (unwrap! (map-get? escrow-accounts { escrow-id: escrow-id }) err-not-found))
            (count-data (get-milestone-count escrow-id))
            (milestone-id (+ (get total count-data) u1))
        )
        (asserts! (is-eq tx-sender (get depositor escrow)) err-unauthorized)
        (asserts! (get requires-milestones escrow) err-invalid-status)
        (asserts! (is-eq (get status escrow) STATUS-ACTIVE) err-invalid-status)

        (map-set escrow-milestones
            { escrow-id: escrow-id, milestone-id: milestone-id }
            {
                description: description,
                amount: amount,
                status: MILESTONE-PENDING,
                condition-hash: condition-hash,
                deadline: deadline,
                completed-at: u0
            }
        )

        (map-set milestone-count
            { escrow-id: escrow-id }
            {
                total: milestone-id,
                completed: (get completed count-data)
            }
        )

        (ok milestone-id)
    )
)

(define-public (complete-milestone
    (escrow-id uint)
    (milestone-id uint)
)
    (let
        (
            (escrow (unwrap! (map-get? escrow-accounts { escrow-id: escrow-id }) err-not-found))
            (milestone (unwrap! (map-get? escrow-milestones { escrow-id: escrow-id, milestone-id: milestone-id }) err-not-found))
            (count-data (get-milestone-count escrow-id))
        )
        (asserts! (is-eq tx-sender (get beneficiary escrow)) err-unauthorized)
        (asserts! (is-eq (get status milestone) MILESTONE-PENDING) err-invalid-status)

        (map-set escrow-milestones
            { escrow-id: escrow-id, milestone-id: milestone-id }
            (merge milestone {
                status: MILESTONE-COMPLETED,
                completed-at: stacks-block-time
            })
        )

        (map-set milestone-count
            { escrow-id: escrow-id }
            {
                total: (get total count-data),
                completed: (+ (get completed count-data) u1)
            }
        )

        (ok true)
    )
)

(define-public (release-funds
    (escrow-id uint)
    (amount uint)
    (reason (string-utf8 200))
)
    (let
        (
            (escrow (unwrap! (map-get? escrow-accounts { escrow-id: escrow-id }) err-not-found))
            (available (- (get amount escrow) (get released-amount escrow)))
            (release-id (+ (var-get milestone-nonce) u1))
        )
        (asserts! (is-eq tx-sender (get depositor escrow)) err-unauthorized)
        (asserts! (is-eq (get status escrow) STATUS-ACTIVE) err-invalid-status)
        (asserts! (<= amount available) err-insufficient-funds)

        ;; In production, transfer STX to beneficiary
        ;; (try! (as-contract (stx-transfer? amount tx-sender (get beneficiary escrow))))

        (map-set escrow-releases
            { escrow-id: escrow-id, release-id: release-id }
            {
                amount: amount,
                recipient: (get beneficiary escrow),
                reason: reason,
                released-at: stacks-block-time
            }
        )

        (let ((new-released (+ (get released-amount escrow) amount)))
            (map-set escrow-accounts
                { escrow-id: escrow-id }
                (merge escrow {
                    released-amount: new-released,
                    status: (if (is-eq new-released (get amount escrow)) STATUS-RELEASED STATUS-PARTIALLY-RELEASED)
                })
            )
        )

        (var-set milestone-nonce release-id)
        (ok true)
    )
)

(define-public (release-milestone-funds
    (escrow-id uint)
    (milestone-id uint)
)
    (let
        (
            (escrow (unwrap! (map-get? escrow-accounts { escrow-id: escrow-id }) err-not-found))
            (milestone (unwrap! (map-get? escrow-milestones { escrow-id: escrow-id, milestone-id: milestone-id }) err-not-found))
            (available (- (get amount escrow) (get released-amount escrow)))
            (release-id (+ (var-get milestone-nonce) u1))
        )
        (asserts! (is-eq tx-sender (get depositor escrow)) err-unauthorized)
        (asserts! (is-eq (get status milestone) MILESTONE-COMPLETED) err-milestone-incomplete)
        (asserts! (<= (get amount milestone) available) err-insufficient-funds)

        ;; In production, transfer STX to beneficiary
        ;; (try! (as-contract (stx-transfer? (get amount milestone) tx-sender (get beneficiary escrow))))

        (map-set escrow-releases
            { escrow-id: escrow-id, release-id: release-id }
            {
                amount: (get amount milestone),
                recipient: (get beneficiary escrow),
                reason: (get description milestone),
                released-at: stacks-block-time
            }
        )

        (let ((new-released (+ (get released-amount escrow) (get amount milestone))))
            (map-set escrow-accounts
                { escrow-id: escrow-id }
                (merge escrow {
                    released-amount: new-released,
                    status: (if (is-eq new-released (get amount escrow)) STATUS-RELEASED STATUS-PARTIALLY-RELEASED)
                })
            )
        )

        (var-set milestone-nonce release-id)
        (ok true)
    )
)

(define-public (refund-escrow (escrow-id uint))
    (let
        (
            (escrow (unwrap! (map-get? escrow-accounts { escrow-id: escrow-id }) err-not-found))
            (refund-amount (- (get amount escrow) (get released-amount escrow)))
        )
        (asserts! (is-eq tx-sender (get depositor escrow)) err-unauthorized)
        (asserts! (>= stacks-block-time (get expiry escrow)) err-escrow-active)
        (asserts! (or
            (is-eq (get status escrow) STATUS-ACTIVE)
            (is-eq (get status escrow) STATUS-PARTIALLY-RELEASED)
        ) err-invalid-status)

        ;; In production, transfer STX back to depositor
        ;; (try! (as-contract (stx-transfer? refund-amount tx-sender (get depositor escrow))))

        (map-set escrow-accounts
            { escrow-id: escrow-id }
            (merge escrow { status: STATUS-REFUNDED })
        )

        (let
            (
                (history (default-to
                    { total-deposited: u0, total-received: u0, total-refunded: u0, escrows-completed: u0 }
                    (map-get? escrow-history { user: (get depositor escrow) })
                ))
            )
            (map-set escrow-history
                { user: (get depositor escrow) }
                (merge history {
                    total-refunded: (+ (get total-refunded history) refund-amount)
                })
            )
        )

        (ok refund-amount)
    )
)

(define-public (emergency-withdrawal (escrow-id uint))
    (let
        (
            (escrow (unwrap! (map-get? escrow-accounts { escrow-id: escrow-id }) err-not-found))
            (available (- (get amount escrow) (get released-amount escrow)))
            (fee (calculate-emergency-fee available))
            (withdrawal-amount (- available fee))
        )
        (asserts! (is-eq tx-sender (get depositor escrow)) err-unauthorized)
        (asserts! (is-eq (get status escrow) STATUS-ACTIVE) err-invalid-status)

        ;; In production, transfer STX minus fee
        ;; (try! (as-contract (stx-transfer? withdrawal-amount tx-sender (get depositor escrow))))
        ;; (try! (as-contract (stx-transfer? fee tx-sender contract-owner)))

        (map-set escrow-accounts
            { escrow-id: escrow-id }
            (merge escrow {
                status: STATUS-REFUNDED,
                released-amount: (get amount escrow)
            })
        )

        (ok withdrawal-amount)
    )
)

(define-public (extend-escrow-period (escrow-id uint) (additional-time uint))
    (let
        (
            (escrow (unwrap! (map-get? escrow-accounts { escrow-id: escrow-id }) err-not-found))
        )
        (asserts! (is-eq tx-sender (get depositor escrow)) err-unauthorized)
        (asserts! (is-eq (get status escrow) STATUS-ACTIVE) err-invalid-status)

        (map-set escrow-accounts
            { escrow-id: escrow-id }
            (merge escrow {
                expiry: (+ (get expiry escrow) additional-time)
            })
        )
        (ok true)
    )
)

;; Admin functions
(define-public (set-min-escrow-amount (new-min uint))
    (begin
        (asserts! (is-eq tx-sender contract-owner) err-owner-only)
        (var-set min-escrow-amount new-min)
        (ok true)
    )
)

(define-public (set-emergency-fee (new-fee uint))
    (begin
        (asserts! (is-eq tx-sender contract-owner) err-owner-only)
        (asserts! (<= new-fee u50) err-invalid-amount) ;; Max 50%
        (var-set emergency-withdrawal-fee new-fee)
        (ok true)
    )
)

Functions (17)

FunctionAccessArgs
set-emergency-feepublicnew-fee: uint
calculate-emergency-feeprivateamount: uint
update-user-pendingprivateuser: principal, amount: uint, is-deposit: bool
get-escrowread-onlyescrow-id: uint
get-milestoneread-onlyescrow-id: uint, milestone-id: uint
get-milestone-countread-onlyescrow-id: uint
get-releaseread-onlyescrow-id: uint, release-id: uint
get-pending-escrowsread-onlyuser: principal
get-escrow-historyread-onlyuser: principal
get-escrow-nonceread-only
is-escrow-expiredread-onlyescrow-id: uint
add-milestonepublicescrow-id: uint, description: (string-utf8 200
release-fundspublicescrow-id: uint, amount: uint, reason: (string-utf8 200
refund-escrowpublicescrow-id: uint
emergency-withdrawalpublicescrow-id: uint
extend-escrow-periodpublicescrow-id: uint, additional-time: uint
set-min-escrow-amountpublicnew-min: uint