Source Code

;; Escrow Milestone Contract
;; Multi-milestone escrow with staged releases
;; Halal - fair project payments
;; Clarity 4 compatible

(define-constant CONTRACT-OWNER tx-sender)
(define-constant ERR-NOT-AUTHORIZED (err u401))
(define-constant ERR-NOT-FOUND (err u404))
(define-constant ERR-ALREADY-RELEASED (err u405))

(define-data-var project-count uint u0)
(define-data-var total-escrowed uint u0)

(define-map projects uint {
  client: principal, provider: principal, total-value: uint,
  released: uint, milestone-count: uint, status: (string-ascii 20), created: uint
})
(define-map milestones { project-id: uint, index: uint } {
  description: (string-utf8 200), amount: uint, status: (string-ascii 20), approved-at: uint
})

(define-public (create-project (provider principal) (total-value uint))
  (let ((id (+ (var-get project-count) u1)))
    (try! (stx-transfer? total-value tx-sender CONTRACT-OWNER))
    (map-set projects id { client: tx-sender, provider: provider, total-value: total-value, released: u0, milestone-count: u0, status: "active", created: stacks-block-height })
    (var-set project-count id)
    (var-set total-escrowed (+ (var-get total-escrowed) total-value))
    (ok id)))

(define-public (add-milestone (project-id uint) (description (string-utf8 200)) (amount uint))
  (let (
    (project (unwrap! (map-get? projects project-id) ERR-NOT-FOUND))
    (idx (get milestone-count project))
  )
    (asserts! (is-eq tx-sender (get client project)) ERR-NOT-AUTHORIZED)
    (map-set milestones { project-id: project-id, index: idx } { description: description, amount: amount, status: "pending", approved-at: u0 })
    (map-set projects project-id (merge project { milestone-count: (+ idx u1) }))
    (ok idx)))

(define-public (submit-milestone (project-id uint) (milestone-index uint))
  (let (
    (project (unwrap! (map-get? projects project-id) ERR-NOT-FOUND))
    (ms (unwrap! (map-get? milestones { project-id: project-id, index: milestone-index }) ERR-NOT-FOUND))
  )
    (asserts! (is-eq tx-sender (get provider project)) ERR-NOT-AUTHORIZED)
    (map-set milestones { project-id: project-id, index: milestone-index } (merge ms { status: "submitted" }))
    (ok true)))

(define-public (approve-milestone (project-id uint) (milestone-index uint))
  (let (
    (project (unwrap! (map-get? projects project-id) ERR-NOT-FOUND))
    (ms (unwrap! (map-get? milestones { project-id: project-id, index: milestone-index }) ERR-NOT-FOUND))
  )
    (asserts! (is-eq tx-sender (get client project)) ERR-NOT-AUTHORIZED)
    (asserts! (is-eq (get status ms) "submitted") ERR-ALREADY-RELEASED)
    (try! (stx-transfer? (get amount ms) CONTRACT-OWNER (get provider project)))
    (map-set milestones { project-id: project-id, index: milestone-index } (merge ms { status: "approved", approved-at: stacks-block-height }))
    (map-set projects project-id (merge project { released: (+ (get released project) (get amount ms)) }))
    (ok (get amount ms))))

(define-public (cancel-project (project-id uint))
  (let (
    (project (unwrap! (map-get? projects project-id) ERR-NOT-FOUND))
    (remaining (- (get total-value project) (get released project)))
  )
    (asserts! (is-eq tx-sender (get client project)) ERR-NOT-AUTHORIZED)
    (if (> remaining u0) (begin (try! (stx-transfer? remaining CONTRACT-OWNER (get client project))) true) true)
    (map-set projects project-id (merge project { status: "cancelled" })) (ok remaining)))

(define-read-only (get-project (id uint)) (map-get? projects id))
(define-read-only (get-milestone (project-id uint) (index uint)) (map-get? milestones { project-id: project-id, index: index }))
(define-read-only (get-project-count) (ok (var-get project-count)))
(define-read-only (get-total-escrowed) (ok (var-get total-escrowed)))

Functions (9)

FunctionAccessArgs
create-projectpublicprovider: principal, total-value: uint
add-milestonepublicproject-id: uint, description: (string-utf8 200
submit-milestonepublicproject-id: uint, milestone-index: uint
approve-milestonepublicproject-id: uint, milestone-index: uint
cancel-projectpublicproject-id: uint
get-projectread-onlyid: uint
get-milestoneread-onlyproject-id: uint, index: uint
get-project-countread-only
get-total-escrowedread-only