Source Code

;; halo-circle.clar
;; Core lending circle (ROSCA) logic
;;
;; Lifecycle: FORMING -> ACTIVE -> COMPLETED
;; - Creator creates circle with parameters, becomes member #1
;; - Members join, assigned sequential positions; collateral locked in vault
;; - Circle auto-activates when full
;; - Each round: all members contribute, recipient (by position) gets payout
;; - After all rounds complete, circle status -> COMPLETED, collateral released
;;
;; Token Support:
;; - STX circles: create-circle, contribute-stx, process-payout
;; - SIP-010 circles: create-token-circle, contribute-token, process-payout-token
;;
;; Cross-contract dependencies:
;; - halo-identity: verifies users have bound wallets
;; - halo-credit: records payments and circle completions for scoring
;; - halo-vault: collateral locking/release for circle participation

(use-trait ft-trait .halo-sip010-trait.sip-010-trait)

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

(define-constant CONTRACT_OWNER tx-sender)
(define-constant ERR_NOT_AUTHORIZED (err u200))
(define-constant ERR_CIRCLE_NOT_FOUND (err u201))
(define-constant ERR_CIRCLE_NOT_FORMING (err u202))
(define-constant ERR_CIRCLE_NOT_ACTIVE (err u203))
(define-constant ERR_ALREADY_MEMBER (err u204))
(define-constant ERR_NOT_MEMBER (err u205))
(define-constant ERR_CIRCLE_FULL (err u206))
(define-constant ERR_INVALID_AMOUNT (err u207))
(define-constant ERR_ALREADY_CONTRIBUTED (err u208))
(define-constant ERR_INVALID_ROUND (err u209))
(define-constant ERR_NOT_VERIFIED (err u210))
(define-constant ERR_TRANSFER_FAILED (err u211))
(define-constant ERR_INVALID_PARAMS (err u212))
(define-constant ERR_CONTRIBUTIONS_INCOMPLETE (err u213))
(define-constant ERR_PAYOUT_ALREADY_PROCESSED (err u214))
(define-constant ERR_NOT_PAUSED (err u215))
(define-constant ERR_NOT_FOUND (err u216))
(define-constant ERR_INSUFFICIENT_COLLATERAL (err u217))
(define-constant ERR_TOKEN_MISMATCH (err u218))
(define-constant ERR_INVALID_TOKEN_TYPE (err u219))

;; Circle status
(define-constant STATUS_FORMING u0)
(define-constant STATUS_ACTIVE u1)
(define-constant STATUS_PAUSED u2)
(define-constant STATUS_COMPLETED u3)
(define-constant STATUS_DISSOLVED u4)

;; Token types
(define-constant TOKEN_TYPE_STX u0)
(define-constant TOKEN_TYPE_SIP010 u1)

;; Limits
(define-constant MIN_MEMBERS u3)
(define-constant MAX_MEMBERS u10)
(define-constant MIN_ROUND_DURATION u144)  ;; ~1 day in blocks
(define-constant MIN_CONTRIBUTION u1000000) ;; 1 STX minimum (in microSTX)

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

(define-data-var admin principal CONTRACT_OWNER)
(define-data-var circle-counter uint u0)
(define-data-var protocol-fee-rate uint u100) ;; 1% = 100 basis points (out of 10000)
(define-data-var temp-circle-id uint u0)       ;; Context for mapped member functions

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

;; Circle ID -> Circle Data
(define-map circles uint {
  name: (string-ascii 30),
  creator: principal,
  contribution-amount: uint,
  total-members: uint,
  current-round: uint,
  status: uint,
  created-at: uint,
  start-block: uint,
  round-duration: uint,
  grace-period: uint,
  total-contributed: uint,
  total-paid-out: uint,
  token-type: uint,
  token-contract: (optional principal)
})

;; (Circle ID, Member) -> Member Data
(define-map circle-members { circle-id: uint, member: principal } {
  unique-id: (buff 32),
  joined-at: uint,
  payout-position: uint,
  total-contributed: uint,
  has-received-payout: bool
})

;; (Circle ID, Member, Round) -> Contribution record
(define-map contributions { circle-id: uint, member: principal, round: uint } {
  amount: uint,
  contributed-at: uint,
  on-time: bool
})

;; Circle ID -> ordered list of member principals
(define-map circle-member-list uint (list 10 principal))

;; (Circle ID, Round) -> Payout record
(define-map payouts { circle-id: uint, round: uint } {
  recipient: principal,
  amount: uint,
  paid-at: uint
})

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

(define-read-only (get-circle (circle-id uint))
  (map-get? circles circle-id)
)

(define-read-only (get-member (circle-id uint) (member principal))
  (map-get? circle-members { circle-id: circle-id, member: member })
)

(define-read-only (get-contribution (circle-id uint) (member principal) (round uint))
  (map-get? contributions { circle-id: circle-id, member: member, round: round })
)

(define-read-only (get-circle-members (circle-id uint))
  (default-to (list) (map-get? circle-member-list circle-id))
)

(define-read-only (get-payout (circle-id uint) (round uint))
  (map-get? payouts { circle-id: circle-id, round: round })
)

(define-read-only (get-circle-count)
  (var-get circle-counter)
)

(define-read-only (get-protocol-fee-rate)
  (var-get protocol-fee-rate)
)

(define-read-only (get-admin)
  (var-get admin)
)

;; Check if user has a bound wallet (verified identity)
(define-read-only (is-verified (user principal))
  (is-some (contract-call? .halo-identity get-id-by-wallet user))
)

;; Check if a payment would be considered on-time for the current round
(define-read-only (is-payment-on-time (circle-id uint))
  (match (map-get? circles circle-id)
    circle (let (
      (round-start (+ (get start-block circle)
                      (* (get current-round circle) (get round-duration circle))))
      (grace-deadline (+ round-start (get grace-period circle)))
    )
      (<= stacks-block-height grace-deadline)
    )
    false
  )
)

;; Get the deadline block for a specific round
(define-read-only (get-round-deadline (circle-id uint) (round uint))
  (match (map-get? circles circle-id)
    circle (some (+ (get start-block circle)
                    (* (+ round u1) (get round-duration circle))))
    none
  )
)

;; Count how many members have contributed in a given round
(define-read-only (count-round-contributions (circle-id uint) (round uint))
  (let (
    (members (get-circle-members circle-id))
  )
    (get count (fold count-member-contribution members
      { circle-id: circle-id, round: round, count: u0 }))
  )
)

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

;; Create a new STX lending circle
(define-public (create-circle
  (name (string-ascii 30))
  (contribution-amount uint)
  (total-members uint)
  (round-duration uint)
  (grace-period uint)
)
  (let (
    (caller tx-sender)
    (new-id (+ (var-get circle-counter) u1))
  )
    ;; Verify caller has bound wallet
    (asserts! (is-verified caller) ERR_NOT_VERIFIED)

    ;; Validate parameters
    (asserts! (>= total-members MIN_MEMBERS) ERR_INVALID_PARAMS)
    (asserts! (<= total-members MAX_MEMBERS) ERR_INVALID_PARAMS)
    (asserts! (>= contribution-amount MIN_CONTRIBUTION) ERR_INVALID_AMOUNT)
    (asserts! (>= round-duration MIN_ROUND_DURATION) ERR_INVALID_PARAMS)
    (asserts! (> grace-period u0) ERR_INVALID_PARAMS)

    ;; Create circle (STX type)
    (map-set circles new-id {
      name: name,
      creator: caller,
      contribution-amount: contribution-amount,
      total-members: total-members,
      current-round: u0,
      status: STATUS_FORMING,
      created-at: stacks-block-height,
      start-block: u0,
      round-duration: round-duration,
      grace-period: grace-period,
      total-contributed: u0,
      total-paid-out: u0,
      token-type: TOKEN_TYPE_STX,
      token-contract: none
    })

    ;; Initialize empty member list
    (map-set circle-member-list new-id (list))

    ;; Update counter
    (var-set circle-counter new-id)

    ;; Add creator as member #1
    (try! (internal-add-member new-id caller u1))

    ;; Lock collateral for creator via vault
    (let (
      (commitment-usd (try! (contract-call? .halo-vault calculate-commitment-usd
                              contribution-amount total-members CONTRACT_OWNER)))
    )
      (try! (contract-call? .halo-vault lock-collateral caller new-id commitment-usd))
    )

    (print {
      event: "circle-created",
      circle-id: new-id,
      creator: caller,
      name: name,
      contribution-amount: contribution-amount,
      total-members: total-members,
      round-duration: round-duration,
      grace-period: grace-period,
      token-type: TOKEN_TYPE_STX
    })

    (ok new-id)
  )
)

;; Create a new SIP-010 token lending circle
(define-public (create-token-circle
  (name (string-ascii 30))
  (token <ft-trait>)
  (contribution-amount uint)
  (total-members uint)
  (round-duration uint)
  (grace-period uint)
)
  (let (
    (caller tx-sender)
    (new-id (+ (var-get circle-counter) u1))
    (token-principal (contract-of token))
  )
    ;; Verify caller has bound wallet
    (asserts! (is-verified caller) ERR_NOT_VERIFIED)

    ;; Validate parameters
    (asserts! (>= total-members MIN_MEMBERS) ERR_INVALID_PARAMS)
    (asserts! (<= total-members MAX_MEMBERS) ERR_INVALID_PARAMS)
    (asserts! (> contribution-amount u0) ERR_INVALID_AMOUNT)
    (asserts! (>= round-duration MIN_ROUND_DURATION) ERR_INVALID_PARAMS)
    (asserts! (> grace-period u0) ERR_INVALID_PARAMS)

    ;; Create circle (SIP-010 type)
    (map-set circles new-id {
      name: name,
      creator: caller,
      contribution-amount: contribution-amount,
      total-members: total-members,
      current-round: u0,
      status: STATUS_FORMING,
      created-at: stacks-block-height,
      start-block: u0,
      round-duration: round-duration,
      grace-period: grace-period,
      total-contributed: u0,
      total-paid-out: u0,
      token-type: TOKEN_TYPE_SIP010,
      token-contract: (some token-principal)
    })

    ;; Initialize empty member list
    (map-set circle-member-list new-id (list))

    ;; Update counter
    (var-set circle-counter new-id)

    ;; Add creator as member #1
    (try! (internal-add-member new-id caller u1))

    ;; Lock collateral for creator via vault
    (let (
      (commitment-usd (try! (contract-call? .halo-vault calculate-commitment-usd
                              contribution-amount total-members token-principal)))
    )
      (try! (contract-call? .halo-vault lock-collateral caller new-id commitment-usd))
    )

    (print {
      event: "circle-created",
      circle-id: new-id,
      creator: caller,
      name: name,
      contribution-amount: contribution-amount,
      total-members: total-members,
      round-duration: round-duration,
      grace-period: grace-period,
      token-type: TOKEN_TYPE_SIP010,
      token-contract: token-principal
    })

    (ok new-id)
  )
)

;; Join an existing circle
(define-public (join-circle (circle-id uint))
  (let (
    (caller tx-sender)
    (circle (unwrap! (map-get? circles circle-id) ERR_CIRCLE_NOT_FOUND))
    (current-members (get-circle-members circle-id))
    (member-count (len current-members))
    (new-position (+ member-count u1))
  )
    ;; Verify caller has bound wallet
    (asserts! (is-verified caller) ERR_NOT_VERIFIED)

    ;; Check circle is forming
    (asserts! (is-eq (get status circle) STATUS_FORMING) ERR_CIRCLE_NOT_FORMING)

    ;; Check not already a member
    (asserts! (is-none (map-get? circle-members { circle-id: circle-id, member: caller }))
              ERR_ALREADY_MEMBER)

    ;; Check not full
    (asserts! (< member-count (get total-members circle)) ERR_CIRCLE_FULL)

    ;; Lock collateral for this member via vault
    (let (
      (price-key (if (is-eq (get token-type circle) TOKEN_TYPE_STX)
                    CONTRACT_OWNER
                    (unwrap! (get token-contract circle) ERR_INVALID_TOKEN_TYPE)))
      (commitment-usd (try! (contract-call? .halo-vault calculate-commitment-usd
                              (get contribution-amount circle)
                              (get total-members circle)
                              price-key)))
    )
      (try! (contract-call? .halo-vault lock-collateral caller circle-id commitment-usd))
    )

    ;; Add member
    (try! (internal-add-member circle-id caller new-position))

    ;; Auto-activate if full
    (if (is-eq new-position (get total-members circle))
      (try! (internal-activate-circle circle-id))
      true
    )

    (print {
      event: "member-joined",
      circle-id: circle-id,
      member: caller,
      position: new-position
    })

    (ok new-position)
  )
)

;; Make STX contribution for current round
(define-public (contribute-stx (circle-id uint))
  (let (
    (caller tx-sender)
    (circle (unwrap! (map-get? circles circle-id) ERR_CIRCLE_NOT_FOUND))
    (member-data (unwrap! (map-get? circle-members { circle-id: circle-id, member: caller })
                          ERR_NOT_MEMBER))
    (current-round (get current-round circle))
    (contribution-amount (get contribution-amount circle))
    (on-time (is-payment-on-time circle-id))
  )
    ;; Check circle is active
    (asserts! (is-eq (get status circle) STATUS_ACTIVE) ERR_CIRCLE_NOT_ACTIVE)

    ;; Check circle is STX type
    (asserts! (is-eq (get token-type circle) TOKEN_TYPE_STX) ERR_TOKEN_MISMATCH)

    ;; Check not already contributed this round
    (asserts! (is-none (map-get? contributions
                                  { circle-id: circle-id, member: caller, round: current-round }))
              ERR_ALREADY_CONTRIBUTED)

    ;; Transfer STX from caller to contract
    (try! (stx-transfer? contribution-amount caller (as-contract tx-sender)))

    ;; Record contribution
    (map-set contributions { circle-id: circle-id, member: caller, round: current-round } {
      amount: contribution-amount,
      contributed-at: stacks-block-height,
      on-time: on-time
    })

    ;; Update member totals
    (map-set circle-members { circle-id: circle-id, member: caller }
      (merge member-data {
        total-contributed: (+ (get total-contributed member-data) contribution-amount)
      })
    )

    ;; Update circle totals
    (map-set circles circle-id
      (merge circle {
        total-contributed: (+ (get total-contributed circle) contribution-amount)
      })
    )

    ;; Record payment in credit contract
    (let (
      (unique-id (unwrap! (contract-call? .halo-identity get-id-by-wallet caller) ERR_NOT_VERIFIED))
    )
      (try! (contract-call? .halo-credit record-payment
             unique-id
             circle-id
             current-round
             contribution-amount
             on-time))
    )

    (print {
      event: "contribution-made",
      circle-id: circle-id,
      member: caller,
      round: current-round,
      amount: contribution-amount,
      on-time: on-time
    })

    (ok true)
  )
)

;; Make SIP-010 token contribution for current round
(define-public (contribute-token (circle-id uint) (token <ft-trait>))
  (let (
    (caller tx-sender)
    (circle (unwrap! (map-get? circles circle-id) ERR_CIRCLE_NOT_FOUND))
    (member-data (unwrap! (map-get? circle-members { circle-id: circle-id, member: caller })
                          ERR_NOT_MEMBER))
    (current-round (get current-round circle))
    (contribution-amount (get contribution-amount circle))
    (on-time (is-payment-on-time circle-id))
  )
    ;; Check circle is active
    (asserts! (is-eq (get status circle) STATUS_ACTIVE) ERR_CIRCLE_NOT_ACTIVE)

    ;; Check circle is SIP-010 type
    (asserts! (is-eq (get token-type circle) TOKEN_TYPE_SIP010) ERR_INVALID_TOKEN_TYPE)

    ;; Check token matches circle's token
    (asserts! (is-eq (some (contract-of token)) (get token-contract circle)) ERR_TOKEN_MISMATCH)

    ;; Check not already contributed this round
    (asserts! (is-none (map-get? contributions
                                  { circle-id: circle-id, member: caller, round: current-round }))
              ERR_ALREADY_CONTRIBUTED)

    ;; Transfer tokens from caller to contract
    (try! (contract-call? token transfer contribution-amount caller (as-contract tx-sender) none))

    ;; Record contribution
    (map-set contributions { circle-id: circle-id, member: caller, round: current-round } {
      amount: contribution-amount,
      contributed-at: stacks-block-height,
      on-time: on-time
    })

    ;; Update member totals
    (map-set circle-members { circle-id: circle-id, member: caller }
      (merge member-data {
        total-contributed: (+ (get total-contributed member-data) contribution-amount)
      })
    )

    ;; Update circle totals
    (map-set circles circle-id
      (merge circle {
        total-contributed: (+ (get total-contributed circle) contribution-amount)
      })
    )

    ;; Record payment in credit contract
    (let (
      (unique-id (unwrap! (contract-call? .halo-identity get-id-by-wallet caller) ERR_NOT_VERIFIED))
    )
      (try! (contract-call? .halo-credit record-payment
             unique-id
             circle-id
             current-round
             contribution-amount
             on-time))
    )

    (print {
      event: "contribution-made",
      circle-id: circle-id,
      member: caller,
      round: current-round,
      amount: contribution-amount,
      on-time: on-time,
      token-type: TOKEN_TYPE_SIP010
    })

    (ok true)
  )
)

;; Process STX payout for current round (anyone can call when all have contributed)
(define-public (process-payout (circle-id uint))
  (let (
    (circle (unwrap! (map-get? circles circle-id) ERR_CIRCLE_NOT_FOUND))
    (current-round (get current-round circle))
    (members (get-circle-members circle-id))
    (total-members (get total-members circle))
    (contribution-amount (get contribution-amount circle))
    (gross-payout (* contribution-amount total-members))
    (protocol-fee (/ (* gross-payout (var-get protocol-fee-rate)) u10000))
    (net-payout (- gross-payout protocol-fee))
    (contribution-count (count-round-contributions circle-id current-round))
  )
    ;; Check circle is active
    (asserts! (is-eq (get status circle) STATUS_ACTIVE) ERR_CIRCLE_NOT_ACTIVE)

    ;; Check circle is STX type
    (asserts! (is-eq (get token-type circle) TOKEN_TYPE_STX) ERR_TOKEN_MISMATCH)

    ;; Check payout not already processed for this round
    (asserts! (is-none (map-get? payouts { circle-id: circle-id, round: current-round }))
              ERR_PAYOUT_ALREADY_PROCESSED)

    ;; Check ALL members have contributed
    (asserts! (is-eq contribution-count total-members) ERR_CONTRIBUTIONS_INCOMPLETE)

    ;; Find recipient for this round (0-indexed in list)
    (let (
      (recipient (unwrap! (element-at? members current-round) ERR_NOT_FOUND))
    )
      ;; Transfer net payout to recipient
      (try! (as-contract (stx-transfer? net-payout tx-sender recipient)))

      ;; Transfer protocol fee to admin (if any)
      (if (> protocol-fee u0)
        (try! (as-contract (stx-transfer? protocol-fee tx-sender (var-get admin))))
        true
      )

      ;; Record payout
      (map-set payouts { circle-id: circle-id, round: current-round } {
        recipient: recipient,
        amount: net-payout,
        paid-at: stacks-block-height
      })

      ;; Update member payout status
      (let (
        (member-data (unwrap! (map-get? circle-members { circle-id: circle-id, member: recipient })
                              ERR_NOT_MEMBER))
      )
        (map-set circle-members { circle-id: circle-id, member: recipient }
          (merge member-data { has-received-payout: true })
        )
      )

      ;; Advance to next round
      (let (
        (next-round (+ current-round u1))
      )
        (map-set circles circle-id
          (merge circle {
            total-paid-out: (+ (get total-paid-out circle) net-payout),
            current-round: next-round
          })
        )

        ;; Check if circle is complete (all rounds done)
        (if (>= next-round total-members)
          (try! (internal-complete-circle circle-id))
          true
        )
      )

      (print {
        event: "payout-processed",
        circle-id: circle-id,
        round: current-round,
        recipient: recipient,
        gross-amount: gross-payout,
        net-amount: net-payout,
        protocol-fee: protocol-fee
      })

      (ok net-payout)
    )
  )
)

;; Process SIP-010 token payout for current round
(define-public (process-payout-token (circle-id uint) (token <ft-trait>))
  (let (
    (circle (unwrap! (map-get? circles circle-id) ERR_CIRCLE_NOT_FOUND))
    (current-round (get current-round circle))
    (members (get-circle-members circle-id))
    (total-members (get total-members circle))
    (contribution-amount (get contribution-amount circle))
    (gross-payout (* contribution-amount total-members))
    (protocol-fee (/ (* gross-payout (var-get protocol-fee-rate)) u10000))
    (net-payout (- gross-payout protocol-fee))
    (contribution-count (count-round-contributions circle-id current-round))
  )
    ;; Check circle is active
    (asserts! (is-eq (get status circle) STATUS_ACTIVE) ERR_CIRCLE_NOT_ACTIVE)

    ;; Check circle is SIP-010 type
    (asserts! (is-eq (get token-type circle) TOKEN_TYPE_SIP010) ERR_INVALID_TOKEN_TYPE)

    ;; Check token matches circle's token
    (asserts! (is-eq (some (contract-of token)) (get token-contract circle)) ERR_TOKEN_MISMATCH)

    ;; Check payout not already processed
    (asserts! (is-none (map-get? payouts { circle-id: circle-id, round: current-round }))
              ERR_PAYOUT_ALREADY_PROCESSED)

    ;; Check ALL members have contributed
    (asserts! (is-eq contribution-count total-members) ERR_CONTRIBUTIONS_INCOMPLETE)

    ;; Find recipient
    (let (
      (recipient (unwrap! (element-at? members current-round) ERR_NOT_FOUND))
    )
      ;; Transfer net payout to recipient
      (try! (as-contract (contract-call? token transfer net-payout tx-sender recipient none)))

      ;; Transfer protocol fee to admin
      (if (> protocol-fee u0)
        (try! (as-contract (contract-call? token transfer protocol-fee tx-sender (var-get admin) none)))
        true
      )

      ;; Record payout
      (map-set payouts { circle-id: circle-id, round: current-round } {
        recipient: recipient,
        amount: net-payout,
        paid-at: stacks-block-height
      })

      ;; Update member payout status
      (let (
        (member-data (unwrap! (map-get? circle-members { circle-id: circle-id, member: recipient })
                              ERR_NOT_MEMBER))
      )
        (map-set circle-members { circle-id: circle-id, member: recipient }
          (merge member-data { has-received-payout: true })
        )
      )

      ;; Advance to next round
      (let (
        (next-round (+ current-round u1))
      )
        (map-set circles circle-id
          (merge circle {
            total-paid-out: (+ (get total-paid-out circle) net-payout),
            current-round: next-round
          })
        )

        ;; Check if circle is complete
        (if (>= next-round total-members)
          (try! (internal-complete-circle circle-id))
          true
        )
      )

      (print {
        event: "payout-processed",
        circle-id: circle-id,
        round: current-round,
        recipient: recipient,
        gross-amount: gross-payout,
        net-amount: net-payout,
        protocol-fee: protocol-fee,
        token-type: TOKEN_TYPE_SIP010
      })

      (ok net-payout)
    )
  )
)

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

;; Add a member to a circle
(define-private (internal-add-member (circle-id uint) (member principal) (position uint))
  (let (
    (unique-id (unwrap! (contract-call? .halo-identity get-id-by-wallet member) ERR_NOT_VERIFIED))
    (current-members (get-circle-members circle-id))
  )
    ;; Add to member data map
    (map-set circle-members { circle-id: circle-id, member: member } {
      unique-id: unique-id,
      joined-at: stacks-block-height,
      payout-position: position,
      total-contributed: u0,
      has-received-payout: false
    })

    ;; Add to ordered member list
    (map-set circle-member-list circle-id
      (unwrap! (as-max-len? (append current-members member) u10) ERR_CIRCLE_FULL))

    (ok true)
  )
)

;; Activate a circle (called when full)
(define-private (internal-activate-circle (circle-id uint))
  (match (map-get? circles circle-id)
    circle (begin
      (map-set circles circle-id
        (merge circle {
          status: STATUS_ACTIVE,
          start-block: stacks-block-height
        })
      )
      (print {
        event: "circle-activated",
        circle-id: circle-id,
        start-block: stacks-block-height
      })
      (ok true)
    )
    ERR_CIRCLE_NOT_FOUND
  )
)

;; Complete a circle (all rounds done)
(define-private (internal-complete-circle (circle-id uint))
  (match (map-get? circles circle-id)
    circle (begin
      (map-set circles circle-id
        (merge circle { status: STATUS_COMPLETED })
      )

      ;; Set context for mapped functions
      (var-set temp-circle-id circle-id)

      ;; Release collateral and record completion for all members
      (let (
        (members (get-circle-members circle-id))
      )
        (map internal-complete-member members)
      )

      (print {
        event: "circle-completed",
        circle-id: circle-id
      })

      (ok true)
    )
    ERR_CIRCLE_NOT_FOUND
  )
)

;; Release collateral and record credit completion for a single member (used with map)
(define-private (internal-complete-member (member principal))
  (let (
    (cid (var-get temp-circle-id))
  )
    (begin
      ;; Release collateral in vault
      (match (contract-call? .halo-vault release-collateral member cid)
        success true
        error false
      )
      ;; Record credit completion
      (match (contract-call? .halo-identity get-id-by-wallet member)
        unique-id (match (contract-call? .halo-credit record-circle-completion unique-id true)
          success true
          error false
        )
        false
      )
    )
  )
)

;; Helper: count a member's contribution for fold
(define-private (count-member-contribution
  (member principal)
  (state { circle-id: uint, round: uint, count: uint })
)
  (if (is-some (map-get? contributions {
        circle-id: (get circle-id state),
        member: member,
        round: (get round state)
      }))
    (merge state { count: (+ (get count state) u1) })
    state
  )
)

;; ============================================
;; ADMIN FUNCTIONS
;; ============================================

;; Set protocol fee rate (basis points, max 10% = 1000)
(define-public (set-protocol-fee-rate (new-rate uint))
  (begin
    (asserts! (is-eq tx-sender (var-get admin)) ERR_NOT_AUTHORIZED)
    (asserts! (<= new-rate u1000) ERR_INVALID_PARAMS)
    (var-set protocol-fee-rate new-rate)
    (print { event: "fee-rate-updated", new-rate: new-rate })
    (ok true)
  )
)

;; Transfer admin
(define-public (set-admin (new-admin principal))
  (begin
    (asserts! (is-eq tx-sender (var-get admin)) ERR_NOT_AUTHORIZED)
    (var-set admin new-admin)
    (print { event: "admin-transferred", new-admin: new-admin })
    (ok true)
  )
)

;; Emergency: pause an active circle
(define-public (pause-circle (circle-id uint))
  (begin
    (asserts! (is-eq tx-sender (var-get admin)) ERR_NOT_AUTHORIZED)
    (match (map-get? circles circle-id)
      circle (begin
        (asserts! (is-eq (get status circle) STATUS_ACTIVE) ERR_CIRCLE_NOT_ACTIVE)
        (map-set circles circle-id (merge circle { status: STATUS_PAUSED }))
        (print { event: "circle-paused", circle-id: circle-id })
        (ok true)
      )
      ERR_CIRCLE_NOT_FOUND
    )
  )
)

;; Resume a paused circle
(define-public (resume-circle (circle-id uint))
  (begin
    (asserts! (is-eq tx-sender (var-get admin)) ERR_NOT_AUTHORIZED)
    (match (map-get? circles circle-id)
      circle (begin
        (asserts! (is-eq (get status circle) STATUS_PAUSED)
                  ERR_NOT_PAUSED)
        (map-set circles circle-id (merge circle { status: STATUS_ACTIVE }))
        (print { event: "circle-resumed", circle-id: circle-id })
        (ok true)
      )
      ERR_CIRCLE_NOT_FOUND
    )
  )
)

Functions (27)

FunctionAccessArgs
get-circleread-onlycircle-id: uint
get-memberread-onlycircle-id: uint, member: principal
get-contributionread-onlycircle-id: uint, member: principal, round: uint
get-circle-membersread-onlycircle-id: uint
get-payoutread-onlycircle-id: uint, round: uint
get-circle-countread-only
get-protocol-fee-rateread-only
get-adminread-only
is-verifiedread-onlyuser: principal
is-payment-on-timeread-onlycircle-id: uint
get-round-deadlineread-onlycircle-id: uint, round: uint
count-round-contributionsread-onlycircle-id: uint, round: uint
create-circlepublicname: (string-ascii 30
create-token-circlepublicname: (string-ascii 30
join-circlepubliccircle-id: uint
contribute-stxpubliccircle-id: uint
contribute-tokenpubliccircle-id: uint, token: <ft-trait>
process-payoutpubliccircle-id: uint
process-payout-tokenpubliccircle-id: uint, token: <ft-trait>
internal-add-memberprivatecircle-id: uint, member: principal, position: uint
internal-activate-circleprivatecircle-id: uint
internal-complete-circleprivatecircle-id: uint
internal-complete-memberprivatemember: principal
set-protocol-fee-ratepublicnew-rate: uint
set-adminpublicnew-admin: principal
pause-circlepubliccircle-id: uint
resume-circlepubliccircle-id: uint