Source Code

;; STX Escrow - Trustless P2P Trading
;; Clarity 4 (Epoch 3.3) - Uses new Clarity 4 features
;; 
;; New Clarity 4 Features Used:
;; - stacks-block-time: Real timestamps for timelocks
;; - as-contract? (Clarity 4): Secure asset handling with allowances
;;
;; Designed for Stacks Builder Challenge Week 3

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

(define-constant CONTRACT-OWNER tx-sender)
(define-constant ERR-NOT-OWNER (err u100))
(define-constant ERR-ESCROW-NOT-FOUND (err u101))
(define-constant ERR-UNAUTHORIZED (err u102))
(define-constant ERR-ALREADY-RELEASED (err u103))
(define-constant ERR-ALREADY-DISPUTED (err u104))
(define-constant ERR-NOT-DISPUTED (err u105))
(define-constant ERR-INSUFFICIENT-FUNDS (err u106))
(define-constant ERR-INVALID-AMOUNT (err u107))
(define-constant ERR-TIMELOCK-ACTIVE (err u108))
(define-constant ERR-SELF-ESCROW (err u109))
(define-constant ERR-TRANSFER-FAILED (err u110))

;; Platform fee: 0.5%
(define-constant PLATFORM-FEE u5)
(define-constant FEE-DENOMINATOR u1000)

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

(define-data-var escrow-count uint u0)
(define-data-var total-volume uint u0)
(define-data-var total-escrows-completed uint u0)
(define-data-var treasury principal CONTRACT-OWNER)

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

;; Escrow storage - Clarity 4 timestamps for timelocks
(define-map escrows uint {
  depositor: principal,
  beneficiary: principal,
  arbiter: (optional principal),
  amount: uint,
  released-amount: uint,
  description: (string-ascii 200),
  created-time: uint,          ;; Clarity 4: Real timestamp
  timelock-until: uint,        ;; Clarity 4: Real timestamp for unlock
  status: (string-ascii 20),
  created-at: uint,
  completed-at: (optional uint)
})

;; Dispute storage
(define-map disputes uint {
  reason: (string-ascii 500),
  disputed-by: principal,
  disputed-at: uint,           ;; Clarity 4: Real timestamp
  resolution: (optional (string-ascii 200)),
  resolved-at: (optional uint)
})

;; User stats
(define-map user-stats principal {
  escrows-created: uint,
  escrows-received: uint,
  escrows-completed: uint,
  disputes-involved: uint,
  total-deposited: uint,
  total-received: uint,
  last-activity: uint
})

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

(define-private (update-depositor-stats (user principal) (amount uint))
  (let
    (
      (current-stats (default-to 
        { escrows-created: u0, escrows-received: u0, escrows-completed: u0, disputes-involved: u0, total-deposited: u0, total-received: u0, last-activity: u0 }
        (map-get? user-stats user)))
    )
    (map-set user-stats user {
      escrows-created: (+ (get escrows-created current-stats) u1),
      escrows-received: (get escrows-received current-stats),
      escrows-completed: (get escrows-completed current-stats),
      disputes-involved: (get disputes-involved current-stats),
      total-deposited: (+ (get total-deposited current-stats) amount),
      total-received: (get total-received current-stats),
      last-activity: stacks-block-time
    })
  )
)

(define-private (update-beneficiary-stats (user principal) (amount uint))
  (let
    (
      (current-stats (default-to 
        { escrows-created: u0, escrows-received: u0, escrows-completed: u0, disputes-involved: u0, total-deposited: u0, total-received: u0, last-activity: u0 }
        (map-get? user-stats user)))
    )
    (map-set user-stats user {
      escrows-created: (get escrows-created current-stats),
      escrows-received: (+ (get escrows-received current-stats) u1),
      escrows-completed: (+ (get escrows-completed current-stats) u1),
      disputes-involved: (get disputes-involved current-stats),
      total-deposited: (get total-deposited current-stats),
      total-received: (+ (get total-received current-stats) amount),
      last-activity: stacks-block-time
    })
  )
)

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

;; Create a new escrow with real-time timelock (Clarity 4)
(define-public (create-escrow 
    (beneficiary principal)
    (amount uint)
    (description (string-ascii 200))
    (timelock-seconds uint)
    (arbiter (optional principal)))
  (let
    (
      (escrow-id (+ (var-get escrow-count) u1))
      ;; Clarity 4: Use real timestamps
      (current-time stacks-block-time)
      (unlock-time (+ current-time timelock-seconds))
    )
    ;; Validations
    (asserts! (> amount u0) ERR-INVALID-AMOUNT)
    (asserts! (not (is-eq tx-sender beneficiary)) ERR-SELF-ESCROW)
    
    ;; Transfer STX to contract custody (Clarity 4 as-contract? with allowances)
    ;; Get contract principal safely, then transfer TO it
    (let ((contract-principal (unwrap-panic (as-contract? () tx-sender))))
      (try! (stx-transfer? amount tx-sender contract-principal))
    )
    
    ;; Store escrow with Clarity 4 timestamps
    (map-set escrows escrow-id {
      depositor: tx-sender,
      beneficiary: beneficiary,
      arbiter: arbiter,
      amount: amount,
      released-amount: u0,
      description: description,
      created-time: current-time,
      timelock-until: unlock-time,
      status: "active",
      created-at: stacks-block-height,
      completed-at: none
    })
    
    ;; Update counter
    (var-set escrow-count escrow-id)
    (var-set total-volume (+ (var-get total-volume) amount))
    
    ;; Update user stats
    (update-depositor-stats tx-sender amount)
    
    ;; Clarity 4: Better logging with to-ascii
    (print { 
      event: "escrow-created", 
      escrow-id: escrow-id, 
      depositor: tx-sender,
      beneficiary: beneficiary,
      amount: amount,
      unlock-time: unlock-time,
    })
    
    (ok escrow-id)
  )
)

;; Release funds to beneficiary (depositor only)
(define-public (release-escrow (escrow-id uint))
  (let
    (
      (escrow (unwrap! (map-get? escrows escrow-id) ERR-ESCROW-NOT-FOUND))
      (amount (get amount escrow))
      (released (get released-amount escrow))
      (remaining (- amount released))
      (fee-amount (/ (* remaining PLATFORM-FEE) FEE-DENOMINATOR))
      (release-amount (- remaining fee-amount))
      (current-time stacks-block-time)
    )
    ;; Only depositor can release
    (asserts! (is-eq tx-sender (get depositor escrow)) ERR-UNAUTHORIZED)
    (asserts! (is-eq (get status escrow) "active") ERR-ALREADY-RELEASED)
    (asserts! (> remaining u0) ERR-INSUFFICIENT-FUNDS)
    
    ;; Transfer to beneficiary and fee (Clarity 4: with-stx allowance)
    (unwrap! (as-contract? ((with-stx (get amount escrow)))
      (try! (stx-transfer? release-amount tx-sender (get beneficiary escrow)))
      (try! (stx-transfer? fee-amount tx-sender (var-get treasury)))
      true
    ) ERR-TRANSFER-FAILED)
    
    ;; Update escrow
    (map-set escrows escrow-id 
      (merge escrow { 
        released-amount: amount,
        status: "released",
        completed-at: (some current-time)
      })
    )
    
    ;; Update stats
    (var-set total-escrows-completed (+ (var-get total-escrows-completed) u1))
    (update-beneficiary-stats (get beneficiary escrow) release-amount)
    
    (print { 
      event: "escrow-released", 
      escrow-id: escrow-id, 
      beneficiary: (get beneficiary escrow),
      amount: release-amount,
      released-at: current-time
    })
    
    (ok true)
  )
)

;; Partial release
(define-public (partial-release (escrow-id uint) (release-amount uint))
  (let
    (
      (escrow (unwrap! (map-get? escrows escrow-id) ERR-ESCROW-NOT-FOUND))
      (amount (get amount escrow))
      (released (get released-amount escrow))
      (remaining (- amount released))
      (fee-amount (/ (* release-amount PLATFORM-FEE) FEE-DENOMINATOR))
      (net-release (- release-amount fee-amount))
      (current-time stacks-block-time)
    )
    ;; Only depositor can release
    (asserts! (is-eq tx-sender (get depositor escrow)) ERR-UNAUTHORIZED)
    (asserts! (is-eq (get status escrow) "active") ERR-ALREADY-RELEASED)
    (asserts! (<= release-amount remaining) ERR-INSUFFICIENT-FUNDS)
    (asserts! (> release-amount u0) ERR-INVALID-AMOUNT)
    
    ;; Transfer to beneficiary and fee (Clarity 4: with-stx allowance)
    (unwrap! (as-contract? ((with-stx release-amount))
      (try! (stx-transfer? net-release tx-sender (get beneficiary escrow)))
      (try! (stx-transfer? fee-amount tx-sender (var-get treasury)))
      true
    ) ERR-TRANSFER-FAILED)
    
    ;; Update escrow
    (map-set escrows escrow-id 
      (merge escrow { 
        released-amount: (+ released release-amount)
      })
    )
    
    ;; Update beneficiary stats
    (update-beneficiary-stats (get beneficiary escrow) net-release)
    
    (print { 
      event: "partial-release", 
      escrow-id: escrow-id, 
      amount: net-release,
      remaining: (- remaining release-amount),
      timestamp: current-time
    })
    
    (ok true)
  )
)

;; Refund to depositor (Clarity 4: time-based check)
(define-public (refund-escrow (escrow-id uint))
  (let
    (
      (escrow (unwrap! (map-get? escrows escrow-id) ERR-ESCROW-NOT-FOUND))
      (amount (get amount escrow))
      (released (get released-amount escrow))
      (remaining (- amount released))
      ;; Clarity 4: Use real timestamp for timelock check
      (current-time stacks-block-time)
    )
    ;; Only depositor can refund
    (asserts! (is-eq tx-sender (get depositor escrow)) ERR-UNAUTHORIZED)
    (asserts! (is-eq (get status escrow) "active") ERR-ALREADY-RELEASED)
    ;; Clarity 4: Time-based timelock check
    (asserts! (> current-time (get timelock-until escrow)) ERR-TIMELOCK-ACTIVE)
    (asserts! (> remaining u0) ERR-INSUFFICIENT-FUNDS)
    
    ;; Transfer back to depositor (Clarity 4: with-stx allowance)
    (unwrap! (as-contract? ((with-stx remaining))
      (try! (stx-transfer? remaining tx-sender (get depositor escrow)))
      true
    ) ERR-TRANSFER-FAILED)
    
    ;; Update escrow
    (map-set escrows escrow-id 
      (merge escrow { 
        status: "refunded",
        completed-at: (some current-time)
      })
    )
    
    (print { 
      event: "escrow-refunded", 
      escrow-id: escrow-id, 
      depositor: (get depositor escrow),
      amount: remaining,
      refunded-at: current-time
    })
    
    (ok true)
  )
)

;; Raise a dispute
(define-public (raise-dispute (escrow-id uint) (reason (string-ascii 500)))
  (let
    (
      (escrow (unwrap! (map-get? escrows escrow-id) ERR-ESCROW-NOT-FOUND))
      (current-time stacks-block-time)
    )
    ;; Either depositor or beneficiary can dispute
    (asserts! 
      (or 
        (is-eq tx-sender (get depositor escrow))
        (is-eq tx-sender (get beneficiary escrow))
      )
      ERR-UNAUTHORIZED
    )
    (asserts! (is-eq (get status escrow) "active") ERR-ALREADY-RELEASED)
    (asserts! (is-none (map-get? disputes escrow-id)) ERR-ALREADY-DISPUTED)
    
    ;; Create dispute with Clarity 4 timestamp
    (map-set disputes escrow-id {
      reason: reason,
      disputed-by: tx-sender,
      disputed-at: current-time,
      resolution: none,
      resolved-at: none
    })
    
    ;; Update escrow status
    (map-set escrows escrow-id 
      (merge escrow { status: "disputed" })
    )
    
    ;; Update user stats
    (let
      (
        (current-stats (default-to 
          { escrows-created: u0, escrows-received: u0, escrows-completed: u0, disputes-involved: u0, total-deposited: u0, total-received: u0, last-activity: u0 }
          (map-get? user-stats tx-sender)))
      )
      (map-set user-stats tx-sender 
        (merge current-stats { 
          disputes-involved: (+ (get disputes-involved current-stats) u1),
          last-activity: current-time
        })
      )
    )
    
    (print { 
      event: "dispute-raised", 
      escrow-id: escrow-id, 
      disputed-by: tx-sender,
      reason: reason,
      timestamp: current-time
    })
    
    (ok true)
  )
)

;; Resolve dispute (arbiter only)
(define-public (resolve-dispute 
    (escrow-id uint) 
    (resolution (string-ascii 200))
    (release-to-beneficiary bool))
  (let
    (
      (escrow (unwrap! (map-get? escrows escrow-id) ERR-ESCROW-NOT-FOUND))
      (dispute (unwrap! (map-get? disputes escrow-id) ERR-NOT-DISPUTED))
      (amount (- (get amount escrow) (get released-amount escrow)))
      (fee-amount (/ (* amount PLATFORM-FEE) FEE-DENOMINATOR))
      (net-amount (- amount fee-amount))
      (current-time stacks-block-time)
    )
    ;; Only arbiter can resolve
    (asserts! (is-some (get arbiter escrow)) ERR-UNAUTHORIZED)
    (asserts! (is-eq tx-sender (unwrap-panic (get arbiter escrow))) ERR-UNAUTHORIZED)
    (asserts! (is-eq (get status escrow) "disputed") ERR-NOT-DISPUTED)
    
    ;; Transfer based on resolution and fee (Clarity 4: with-stx allowance)
    (unwrap! (as-contract? ((with-stx (get amount escrow)))
      (try! (if release-to-beneficiary
        (stx-transfer? net-amount tx-sender (get beneficiary escrow))
        (stx-transfer? net-amount tx-sender (get depositor escrow))
      ))
      (try! (stx-transfer? fee-amount tx-sender (var-get treasury)))
      true
    ) ERR-TRANSFER-FAILED)
    
    ;; Update dispute
    (map-set disputes escrow-id 
      (merge dispute { 
        resolution: (some resolution),
        resolved-at: (some current-time)
      })
    )
    
    ;; Update escrow
    (map-set escrows escrow-id 
      (merge escrow { 
        status: "resolved",
        completed-at: (some current-time)
      })
    )
    
    (print { 
      event: "dispute-resolved", 
      escrow-id: escrow-id, 
      resolution: resolution,
      released-to: (if release-to-beneficiary (get beneficiary escrow) (get depositor escrow)),
      resolved-at: current-time
    })
    
    (ok true)
  )
)

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

(define-read-only (get-escrow (escrow-id uint))
  (map-get? escrows escrow-id)
)

(define-read-only (get-dispute (escrow-id uint))
  (map-get? disputes escrow-id)
)

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

(define-read-only (get-user-stats (user principal))
  (default-to 
    { escrows-created: u0, escrows-received: u0, escrows-completed: u0, disputes-involved: u0, total-deposited: u0, total-received: u0, last-activity: u0 }
    (map-get? user-stats user))
)

(define-read-only (get-total-stats)
  {
    total-escrows: (var-get escrow-count),
    total-volume: (var-get total-volume),
    total-completed: (var-get total-escrows-completed)
  }
)

(define-read-only (is-escrow-active (escrow-id uint))
  (let
    (
      (escrow (map-get? escrows escrow-id))
    )
    (if (is-none escrow)
      false
      (is-eq (get status (unwrap-panic escrow)) "active")
    )
  )
)

;; Clarity 4: Time-based refund eligibility
(define-read-only (can-refund (escrow-id uint))
  (let
    (
      (escrow (map-get? escrows escrow-id))
      (current-time stacks-block-time)
    )
    (if (is-none escrow)
      false
      (let
        (
          (e (unwrap-panic escrow))
        )
        (and
          (is-eq (get status e) "active")
          (> current-time (get timelock-until e))
        )
      )
    )
  )
)

;; Get time until unlock (Clarity 4)
(define-read-only (get-time-until-unlock (escrow-id uint))
  (let
    (
      (escrow (map-get? escrows escrow-id))
      (current-time stacks-block-time)
    )
    (if (is-none escrow)
      u0
      (let
        (
          (e (unwrap-panic escrow))
          (unlock-time (get timelock-until e))
        )
        (if (> unlock-time current-time)
          (- unlock-time current-time)
          u0
        )
      )
    )
  )
)

(define-read-only (get-remaining-amount (escrow-id uint))
  (let
    (
      (escrow (map-get? escrows escrow-id))
    )
    (if (is-none escrow)
      u0
      (let
        (
          (e (unwrap-panic escrow))
        )
        (- (get amount e) (get released-amount e))
      )
    )
  )
)

;; Get current time (Clarity 4)
(define-read-only (get-current-time)
  stacks-block-time
)

Functions (18)

FunctionAccessArgs
update-depositor-statsprivateuser: principal, amount: uint
update-beneficiary-statsprivateuser: principal, amount: uint
create-escrowpublicbeneficiary: principal, amount: uint, description: (string-ascii 200
release-escrowpublicescrow-id: uint
partial-releasepublicescrow-id: uint, release-amount: uint
refund-escrowpublicescrow-id: uint
raise-disputepublicescrow-id: uint, reason: (string-ascii 500
resolve-disputepublicescrow-id: uint, resolution: (string-ascii 200
get-escrowread-onlyescrow-id: uint
get-disputeread-onlyescrow-id: uint
get-escrow-countread-only
get-user-statsread-onlyuser: principal
get-total-statsread-only
is-escrow-activeread-onlyescrow-id: uint
can-refundread-onlyescrow-id: uint
get-time-until-unlockread-onlyescrow-id: uint
get-remaining-amountread-onlyescrow-id: uint
get-current-timeread-only