Source Code

;; StackSusu Core v7
;; Main savings circle contract - simplified, no authorization barriers

(define-constant CONTRACT-OWNER tx-sender)

;; Error constants  
(define-constant ERR-CIRCLE-NOT-FOUND (err u1001))
(define-constant ERR-NOT-MEMBER (err u1002))
(define-constant ERR-ALREADY-MEMBER (err u1003))
(define-constant ERR-CIRCLE-FULL (err u1004))
(define-constant ERR-INVALID-AMOUNT (err u1005))
(define-constant ERR-INVALID-MEMBERS (err u1006))
(define-constant ERR-NOT-CREATOR (err u1007))
(define-constant ERR-INVALID-INTERVAL (err u1008))
(define-constant ERR-CIRCLE-NOT-ACTIVE (err u1009))
(define-constant ERR-ALREADY-CONTRIBUTED (err u1010))
(define-constant ERR-CIRCLE-ACTIVE (err u1011))
(define-constant ERR-NOT-ENOUGH-MEMBERS (err u1012))
(define-constant ERR-PAUSED (err u1021))
(define-constant ERR-TRANSFER-FAILED (err u1017))
(define-constant ERR-SLOT-TAKEN (err u1030))
(define-constant ERR-MAX-CIRCLES-REACHED (err u1031))
(define-constant ERR-INVALID-MODE (err u1032))

;; Circle status
(define-constant STATUS-PENDING u0)
(define-constant STATUS-ACTIVE u1)
(define-constant STATUS-COMPLETED u2)
(define-constant STATUS-CANCELLED u3)

;; Contribution modes
(define-constant MODE-MANUAL u0)
(define-constant MODE-SCHEDULED u1)

;; Configuration
(define-constant MIN-MEMBERS u2)
(define-constant MAX-MEMBERS u12)
(define-constant MIN-CONTRIBUTION u100000)      ;; 0.1 STX minimum
(define-constant MAX-CONTRIBUTION u100000000000) ;; 100k STX max
(define-constant BLOCKS-PER-DAY u144)
(define-constant MAX-CIRCLES-PER-MEMBER u20)

;; State
(define-data-var circle-counter uint u0)

;; Circle data
(define-map circles
  uint  ;; circle-id
  {
    creator: principal,
    name: (string-ascii 50),
    description: (string-ascii 200),
    contribution: uint,
    max-members: uint,
    payout-interval: uint,
    status: uint,
    current-round: uint,
    start-block: uint,
    member-count: uint,
    created-at: uint,
    contribution-mode: uint,
    min-reputation: uint,
    total-contributed: uint,
    total-paid-out: uint,
    current-pot: uint
  }
)

;; Circle membership
(define-map circle-members
  { circle-id: uint, member: principal }
  { 
    slot: uint,
    joined-at: uint,
    contributions-made: uint,
    last-contribution-round: uint
  }
)

;; Slot to member mapping
(define-map slot-to-member
  { circle-id: uint, slot: uint }
  principal
)

;; Round contributions
(define-map round-contributions
  { circle-id: uint, round: uint, member: principal }
  { amount: uint, contributed-at: uint }
)

;; Member's circles
(define-map member-circles
  principal
  (list 20 uint)
)


;; ============================================
;; Create Circle
;; ============================================

(define-public (create-circle 
    (name (string-ascii 50))
    (description (string-ascii 200))
    (contribution uint)
    (max-members uint)
    (payout-interval uint))
  (let
    (
      (creator tx-sender)
      (circle-id (+ (var-get circle-counter) u1))
      (current-circles (default-to (list) (map-get? member-circles creator)))
    )
    ;; Validations
    (asserts! (not (contract-call? .stacksusu-admin-v7 is-paused)) ERR-PAUSED)
    (asserts! (and (>= max-members MIN-MEMBERS) (<= max-members MAX-MEMBERS)) ERR-INVALID-MEMBERS)
    (asserts! (and (>= contribution MIN-CONTRIBUTION) (<= contribution MAX-CONTRIBUTION)) ERR-INVALID-AMOUNT)
    (asserts! (>= payout-interval BLOCKS-PER-DAY) ERR-INVALID-INTERVAL)
    (asserts! (< (len current-circles) MAX-CIRCLES-PER-MEMBER) ERR-MAX-CIRCLES-REACHED)
    
    ;; Create circle
    (map-set circles circle-id {
      creator: creator,
      name: name,
      description: description,
      contribution: contribution,
      max-members: max-members,
      payout-interval: payout-interval,
      status: STATUS-PENDING,
      current-round: u1,
      start-block: u0,
      member-count: u1,
      created-at: block-height,
      contribution-mode: MODE-MANUAL,
      min-reputation: u0,
      total-contributed: u0,
      total-paid-out: u0,
      current-pot: u0
    })
    
    ;; Add creator as first member
    (map-set circle-members { circle-id: circle-id, member: creator }
      { slot: u1, joined-at: block-height, contributions-made: u0, last-contribution-round: u0 })
    (map-set slot-to-member { circle-id: circle-id, slot: u1 } creator)
    
    ;; Track member's circles
    (map-set member-circles creator 
      (unwrap! (as-max-len? (append current-circles circle-id) u20) ERR-MAX-CIRCLES-REACHED))
    
    ;; Initialize reputation
    (try! (contract-call? .stacksusu-reputation-v7 initialize-member creator))
    
    ;; Update admin stats
    (try! (contract-call? .stacksusu-admin-v7 increment-circles))
    
    (var-set circle-counter circle-id)
    (ok circle-id)
  )
)


;; ============================================
;; Join Circle
;; ============================================

(define-public (join-circle (circle-id uint))
  (let
    (
      (circle (unwrap! (map-get? circles circle-id) ERR-CIRCLE-NOT-FOUND))
      (member tx-sender)
      (current-count (get member-count circle))
      (new-slot (+ current-count u1))
      (current-circles (default-to (list) (map-get? member-circles member)))
    )
    ;; Validations
    (asserts! (not (contract-call? .stacksusu-admin-v7 is-paused)) ERR-PAUSED)
    (asserts! (is-eq (get status circle) STATUS-PENDING) ERR-CIRCLE-ACTIVE)
    (asserts! (is-none (map-get? circle-members { circle-id: circle-id, member: member })) ERR-ALREADY-MEMBER)
    (asserts! (< current-count (get max-members circle)) ERR-CIRCLE-FULL)
    (asserts! (< (len current-circles) MAX-CIRCLES-PER-MEMBER) ERR-MAX-CIRCLES-REACHED)
    
    ;; Check reputation requirement
    (let ((rep-score (unwrap! (contract-call? .stacksusu-reputation-v7 get-score member) ERR-NOT-MEMBER)))
      (asserts! (>= rep-score (get min-reputation circle)) ERR-NOT-MEMBER)
    )
    
    ;; Add member
    (map-set circle-members { circle-id: circle-id, member: member }
      { slot: new-slot, joined-at: block-height, contributions-made: u0, last-contribution-round: u0 })
    (map-set slot-to-member { circle-id: circle-id, slot: new-slot } member)
    
    ;; Update circle
    (map-set circles circle-id (merge circle { member-count: new-slot }))
    
    ;; Track member's circles
    (map-set member-circles member 
      (unwrap! (as-max-len? (append current-circles circle-id) u20) ERR-MAX-CIRCLES-REACHED))
    
    ;; Initialize reputation
    (try! (contract-call? .stacksusu-reputation-v7 initialize-member member))
    (try! (contract-call? .stacksusu-reputation-v7 record-circle-join member))
    
    (ok new-slot)
  )
)


;; ============================================
;; Start Circle
;; ============================================

(define-public (start-circle (circle-id uint))
  (let
    (
      (circle (unwrap! (map-get? circles circle-id) ERR-CIRCLE-NOT-FOUND))
    )
    (asserts! (is-eq tx-sender (get creator circle)) ERR-NOT-CREATOR)
    (asserts! (is-eq (get status circle) STATUS-PENDING) ERR-CIRCLE-ACTIVE)
    (asserts! (>= (get member-count circle) MIN-MEMBERS) ERR-NOT-ENOUGH-MEMBERS)
    
    (map-set circles circle-id (merge circle {
      status: STATUS-ACTIVE,
      start-block: block-height
    }))
    
    (ok true)
  )
)


;; ============================================
;; Contribute
;; ============================================

(define-public (contribute (circle-id uint))
  (let
    (
      (circle (unwrap! (map-get? circles circle-id) ERR-CIRCLE-NOT-FOUND))
      (member tx-sender)
      (member-data (unwrap! (map-get? circle-members { circle-id: circle-id, member: member }) ERR-NOT-MEMBER))
      (round (get current-round circle))
      (contribution-amount (get contribution circle))
      (fee-bps (contract-call? .stacksusu-admin-v7 get-admin-fee-bps))
      (fee (/ (* contribution-amount fee-bps) u10000))
      (net-amount (- contribution-amount fee))
    )
    ;; Validations
    (asserts! (not (contract-call? .stacksusu-admin-v7 is-paused)) ERR-PAUSED)
    (asserts! (is-eq (get status circle) STATUS-ACTIVE) ERR-CIRCLE-NOT-ACTIVE)
    (asserts! (is-none (map-get? round-contributions { circle-id: circle-id, round: round, member: member })) ERR-ALREADY-CONTRIBUTED)
    
    ;; Transfer STX to escrow
    (try! (contract-call? .stacksusu-escrow-v7 deposit circle-id contribution-amount))
    
    ;; Record fee
    (if (> fee u0)
      (try! (contract-call? .stacksusu-admin-v7 record-fee fee))
      true
    )
    
    ;; Record contribution
    (map-set round-contributions { circle-id: circle-id, round: round, member: member }
      { amount: contribution-amount, contributed-at: block-height })
    
    ;; Update member data
    (map-set circle-members { circle-id: circle-id, member: member }
      (merge member-data {
        contributions-made: (+ (get contributions-made member-data) u1),
        last-contribution-round: round
      }))
    
    ;; Update circle pot
    (map-set circles circle-id (merge circle {
      total-contributed: (+ (get total-contributed circle) contribution-amount),
      current-pot: (+ (get current-pot circle) net-amount)
    }))
    
    ;; Update reputation
    (try! (contract-call? .stacksusu-reputation-v7 record-contribution member))
    
    (ok true)
  )
)


;; ============================================
;; Process Payout
;; ============================================

(define-public (process-payout (circle-id uint))
  (let
    (
      (circle (unwrap! (map-get? circles circle-id) ERR-CIRCLE-NOT-FOUND))
      (round (get current-round circle))
      (recipient (unwrap! (map-get? slot-to-member { circle-id: circle-id, slot: round }) ERR-NOT-MEMBER))
      (payout-amount (get current-pot circle))
    )
    ;; Validations
    (asserts! (not (contract-call? .stacksusu-admin-v7 is-paused)) ERR-PAUSED)
    (asserts! (is-eq (get status circle) STATUS-ACTIVE) ERR-CIRCLE-NOT-ACTIVE)
    (asserts! (> payout-amount u0) ERR-INVALID-AMOUNT)
    
    ;; Process payout from escrow
    (try! (contract-call? .stacksusu-escrow-v7 process-payout circle-id recipient payout-amount))
    
    ;; Update circle
    (let
      (
        (new-round (+ round u1))
        (is-complete (> new-round (get member-count circle)))
      )
      (map-set circles circle-id (merge circle {
        current-round: new-round,
        current-pot: u0,
        total-paid-out: (+ (get total-paid-out circle) payout-amount),
        status: (if is-complete STATUS-COMPLETED (get status circle))
      }))
      
      ;; Update stats
      (try! (contract-call? .stacksusu-admin-v7 increment-payouts))
      (try! (contract-call? .stacksusu-reputation-v7 record-payout recipient))
      
      ;; If completed, record for all members
      (if is-complete
        (try! (contract-call? .stacksusu-reputation-v7 record-circle-complete recipient))
        true
      )
      
      (ok payout-amount)
    )
  )
)


;; ============================================
;; Read Functions
;; ============================================

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

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

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

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

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

(define-read-only (get-member-circles (member principal))
  (default-to (list) (map-get? member-circles member))
)

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

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

Functions (13)

FunctionAccessArgs
create-circlepublicname: (string-ascii 50
join-circlepubliccircle-id: uint
start-circlepubliccircle-id: uint
contributepubliccircle-id: uint
process-payoutpubliccircle-id: uint
get-circleread-onlycircle-id: uint
get-circle-countread-only
get-memberread-onlycircle-id: uint, member: principal
get-slot-memberread-onlycircle-id: uint, slot: uint
get-contributionread-onlycircle-id: uint, round: uint, member: principal
get-member-circlesread-onlymember: principal
is-memberread-onlycircle-id: uint, member: principal
has-contributedread-onlycircle-id: uint, round: uint, member: principal