Source Code

;; crowdfunding-campaign.clar
;; Crowdfunding system with goals, deadlines, and refunds

;; Constants
(define-constant CONTRACT-ADDRESS .crowdfunding-campaign)
(define-constant ERR-CAMPAIGN-NOT-FOUND (err u100))
(define-constant ERR-NOT-CREATOR (err u101))
(define-constant ERR-CAMPAIGN-NOT-ACTIVE (err u102))
(define-constant ERR-CAMPAIGN-STILL-ACTIVE (err u103))
(define-constant ERR-INVALID-GOAL (err u104))
(define-constant ERR-ZERO-CONTRIBUTION (err u105))
(define-constant ERR-CAMPAIGN-NOT-FAILED (err u106))
(define-constant ERR-NO-CONTRIBUTION (err u107))
(define-constant ERR-FUNDS-ALREADY-WITHDRAWN (err u108))
(define-constant ERR-CAMPAIGN-NOT-SUCCESSFUL (err u109))

;; Status
(define-constant STATUS-ACTIVE u0)
(define-constant STATUS-SUCCESSFUL u1)
(define-constant STATUS-FAILED u2)

;; Data Variables
(define-data-var campaign-counter uint u0)

;; Maps
(define-map campaigns uint 
  {
    creator: principal,
    title: (string-ascii 64),
    description: (string-ascii 256),
    goal: uint,
    raised: uint,
    deadline: uint,
    status: uint,
    funds-withdrawn: bool,
    contributor-count: uint
  }
)

(define-map user-contributions { campaign-id: uint, user: principal } uint)

;; Public Functions
(define-public (create-campaign (title (string-ascii 64)) (description (string-ascii 256)) (goal uint) (duration-days uint))
  (let (
    (campaign-id (+ (var-get campaign-counter) u1))
    (deadline (+ stacks-block-time (* duration-days u86400)))
  )
    (begin
      (asserts! (> (len title) u0) (err u110))
      (asserts! (> (len description) u0) (err u111))
      (asserts! (> goal u0) ERR-INVALID-GOAL)
      (asserts! (> duration-days u0) ERR-CAMPAIGN-STILL-ACTIVE)
      
      (map-set campaigns campaign-id {
        creator: tx-sender,
        title: title,
        description: description,
        goal: goal,
        raised: u0,
        deadline: deadline,
        status: STATUS-ACTIVE,
        funds-withdrawn: false,
        contributor-count: u0
      })
      
      (var-set campaign-counter campaign-id)
      (print { event: "campaign-created", id: campaign-id, creator: tx-sender, goal: goal, deadline: deadline })
      (ok campaign-id)
    )
  )
)

(define-public (contribute (campaign-id uint) (amount uint))
  (let (
    (campaign (unwrap! (map-get? campaigns campaign-id) ERR-CAMPAIGN-NOT-FOUND))
    (current-contribution (default-to u0 (map-get? user-contributions { campaign-id: campaign-id, user: tx-sender })))
  )
    (begin
      (asserts! (> amount u0) ERR-ZERO-CONTRIBUTION)
      (asserts! (is-eq (get status campaign) STATUS-ACTIVE) ERR-CAMPAIGN-NOT-ACTIVE)
      (asserts! (< stacks-block-time (get deadline campaign)) ERR-CAMPAIGN-STILL-ACTIVE)
      
      (unwrap-panic (stx-transfer? amount tx-sender CONTRACT-ADDRESS))
      
      (let ((new-raised (+ (get raised campaign) amount)))
        (begin
          (map-set user-contributions { campaign-id: campaign-id, user: tx-sender } (+ current-contribution amount))
          (map-set campaigns campaign-id (merge campaign {
            raised: new-raised,
            contributor-count: (if (is-eq current-contribution u0) (+ (get contributor-count campaign) u1) (get contributor-count campaign)),
            status: (if (>= new-raised (get goal campaign)) STATUS-SUCCESSFUL (get status campaign))
          }))
          (print { event: "contribution-made", id: campaign-id, contributor: tx-sender, amount: amount })
          (ok true)
        )
      )
    )
  )
)

(define-public (withdraw-funds (campaign-id uint))
  (let ((campaign (unwrap! (map-get? campaigns campaign-id) ERR-CAMPAIGN-NOT-FOUND)))
    (begin
      (asserts! (is-eq (get status campaign) STATUS-SUCCESSFUL) ERR-CAMPAIGN-NOT-SUCCESSFUL)
      (asserts! (not (get funds-withdrawn campaign)) ERR-FUNDS-ALREADY-WITHDRAWN)
      
      (let ((amount (get raised campaign)))
        (begin
          (map-set campaigns campaign-id (merge campaign { funds-withdrawn: true }))
          (try! (stx-transfer? amount CONTRACT-ADDRESS (get creator campaign)))
          (print { event: "funds-withdrawn", id: campaign-id, creator: (get creator campaign), amount: amount })
          (ok true)
        )
      )
    )
  )
)

(define-public (claim-refund (campaign-id uint))
  (let (
    (campaign (unwrap! (map-get? campaigns campaign-id) ERR-CAMPAIGN-NOT-FOUND))
    (contribution (unwrap! (map-get? user-contributions { campaign-id: campaign-id, user: tx-sender }) ERR-NO-CONTRIBUTION))
  )
    (begin
      ;; Check if failed or expired and not reached goal
      (asserts! (or 
                  (is-eq (get status campaign) STATUS-FAILED)
                  (and (is-eq (get status campaign) STATUS-ACTIVE) (>= stacks-block-time (get deadline campaign)) (< (get raised campaign) (get goal campaign)))) 
                ERR-CAMPAIGN-NOT-FAILED)
      
      (map-delete user-contributions { campaign-id: campaign-id, user: tx-sender })
      (try! (stx-transfer? contribution CONTRACT-ADDRESS tx-sender))
      (print { event: "refund-claimed", id: campaign-id, contributor: tx-sender, amount: contribution })
      (ok true)
    )
  )
)

;; Read-only
(define-read-only (get-campaign (campaign-id uint))
  (map-get? campaigns campaign-id)
)

;; CLARITY 4 FEATURE: to-ascii? for campaign status
(define-read-only (get-campaign-status-ascii (campaign-id uint))
  (match (map-get? campaigns campaign-id)
    campaign (let (
      (raised-ascii (match (to-ascii? (get raised campaign)) ok-val ok-val err "0"))
      (goal-ascii (match (to-ascii? (get goal campaign)) ok-val ok-val err "0"))
      (status-val (get status campaign))
      (status-str (if (is-eq status-val STATUS-ACTIVE) "ACTIVE"
        (if (is-eq status-val STATUS-SUCCESSFUL) "SUCCESSFUL"
        (if (is-eq status-val STATUS-FAILED) "FAILED"
        "CANCELLED"))))
    )
      (ok {
        title: (get title campaign),
        raised: raised-ascii,
        goal: goal-ascii,
        status: status-str
      })
    )
    (ok {
        title: "Campaign not found",
        raised: "0",
        goal: "0",
        status: "NOT_FOUND"
    })
  )
)

Functions (6)

FunctionAccessArgs
create-campaignpublictitle: (string-ascii 64
contributepubliccampaign-id: uint, amount: uint
withdraw-fundspubliccampaign-id: uint
claim-refundpubliccampaign-id: uint
get-campaignread-onlycampaign-id: uint
get-campaign-status-asciiread-onlycampaign-id: uint