Source Code

;; Milestone Manager Smart Contract
;; Manages milestone-based fund releases with backer governance voting

;; Error codes
(define-constant ERR-NOT-AUTHORIZED (err u300))
(define-constant ERR-CAMPAIGN-NOT-FOUND (err u301))
(define-constant ERR-MILESTONE-NOT-FOUND (err u302))
(define-constant ERR-INVALID-AMOUNT (err u303))
(define-constant ERR-INVALID-MILESTONE (err u304))
(define-constant ERR-MILESTONE-ALREADY-RELEASED (err u305))
(define-constant ERR-MILESTONE-NOT-APPROVED (err u306))
(define-constant ERR-VOTING-ENDED (err u307))
(define-constant ERR-ALREADY-VOTED (err u308))
(define-constant ERR-NOT-BACKER (err u309))
(define-constant ERR-INSUFFICIENT-FUNDS (err u310))
(define-constant ERR-INVALID-VOTING-PERIOD (err u311))
(define-constant ERR-VOTING-ACTIVE (err u312))
(define-constant ERR-CAMPAIGN-NOT-SUCCESSFUL (err u313))
(define-constant ERR-TRANSFER-FAILED (err u314))

;; Milestone status constants
(define-constant STATUS-PENDING u1)
(define-constant STATUS-VOTING u2)
(define-constant STATUS-APPROVED u3)
(define-constant STATUS-REJECTED u4)
(define-constant STATUS-RELEASED u5)
(define-constant STATUS-CANCELLED u6)

;; Voting constants
(define-constant VOTE-YES u1)
(define-constant VOTE-NO u2)
(define-constant MIN-APPROVAL-PERCENTAGE u51) ;; 51% approval required

;; Data variables
(define-data-var milestone-nonce uint u0)

;; Data maps

;; Campaign milestone configuration
(define-map campaign-milestone-config
    uint
    {
        total-milestones: uint,
        completed-milestones: uint,
        total-allocated: uint,
        total-released: uint,
        milestone-enabled: bool
    }
)

;; Milestone details
(define-map milestones
    { campaign-id: uint, milestone-id: uint }
    {
        title: (string-ascii 100),
        description: (string-utf8 500),
        amount: uint,
        status: uint,
        created-at: uint,
        voting-deadline: uint,
        released-at: (optional uint),
        creator: principal
    }
)

;; Voting records
(define-map milestone-votes
    { campaign-id: uint, milestone-id: uint }
    {
        yes-votes: uint,
        no-votes: uint,
        total-voters: uint,
        total-voting-power: uint,
        voting-start: uint,
        voting-end: uint,
        approved: bool
    }
)

;; Individual voter records
(define-map voter-records
    { campaign-id: uint, milestone-id: uint, voter: principal }
    {
        vote: uint,
        voting-power: uint,
        voted-at: uint
    }
)

;; Milestone deliverables/proof
(define-map milestone-deliverables
    { campaign-id: uint, milestone-id: uint }
    {
        proof-url: (string-utf8 256),
        submitted-at: uint,
        notes: (string-utf8 500)
    }
)

;; Campaign voting power tracking
(define-map campaign-voting-power
    { campaign-id: uint, backer: principal }
    {
        contribution-amount: uint,
        voting-power: uint
    }
)

;; Private functions

(define-private (calculate-voting-power (contribution-amount uint))
    ;; Simple linear voting power based on contribution
    ;; Could be enhanced with quadratic voting or other mechanisms
    contribution-amount
)

(define-private (has-voting-ended (campaign-id uint) (milestone-id uint))
    (match (map-get? milestone-votes { campaign-id: campaign-id, milestone-id: milestone-id })
        vote-data (>= stacks-block-height (get voting-end vote-data))
        true
    )
)

(define-private (calculate-approval-percentage (yes-votes uint) (total-voting-power uint))
    (if (is-eq total-voting-power u0)
        u0
        (/ (* yes-votes u100) total-voting-power)
    )
)

(define-private (is-milestone-approved (campaign-id uint) (milestone-id uint))
    (match (map-get? milestone-votes { campaign-id: campaign-id, milestone-id: milestone-id })
        vote-data 
            (let (
                (approval-pct (calculate-approval-percentage 
                    (get yes-votes vote-data) 
                    (get total-voting-power vote-data)
                ))
            )
                (>= approval-pct MIN-APPROVAL-PERCENTAGE)
            )
        false
    )
)

;; Read-only functions

(define-read-only (get-campaign-milestone-config (campaign-id uint))
    (map-get? campaign-milestone-config campaign-id)
)

(define-read-only (get-milestone (campaign-id uint) (milestone-id uint))
    (map-get? milestones { campaign-id: campaign-id, milestone-id: milestone-id })
)

(define-read-only (get-milestone-votes (campaign-id uint) (milestone-id uint))
    (map-get? milestone-votes { campaign-id: campaign-id, milestone-id: milestone-id })
)

(define-read-only (get-voter-record (campaign-id uint) (milestone-id uint) (voter principal))
    (map-get? voter-records { 
        campaign-id: campaign-id, 
        milestone-id: milestone-id, 
        voter: voter 
    })
)

(define-read-only (get-milestone-deliverables (campaign-id uint) (milestone-id uint))
    (map-get? milestone-deliverables { campaign-id: campaign-id, milestone-id: milestone-id })
)

(define-read-only (get-voting-power (campaign-id uint) (backer principal))
    (match (map-get? campaign-voting-power { campaign-id: campaign-id, backer: backer })
        power-data (ok (get voting-power power-data))
        (ok u0)
    )
)

(define-read-only (get-milestone-status (campaign-id uint) (milestone-id uint))
    (match (get-milestone campaign-id milestone-id)
        milestone (ok (get status milestone))
        ERR-MILESTONE-NOT-FOUND
    )
)

(define-read-only (get-voting-results (campaign-id uint) (milestone-id uint))
    (match (get-milestone-votes campaign-id milestone-id)
        vote-data 
            (ok {
                yes-votes: (get yes-votes vote-data),
                no-votes: (get no-votes vote-data),
                total-voters: (get total-voters vote-data),
                approval-percentage: (calculate-approval-percentage 
                    (get yes-votes vote-data) 
                    (get total-voting-power vote-data)
                ),
                approved: (get approved vote-data),
                voting-ended: (has-voting-ended campaign-id milestone-id)
            })
        ERR-MILESTONE-NOT-FOUND
    )
)

(define-read-only (has-voted (campaign-id uint) (milestone-id uint) (voter principal))
    (is-some (get-voter-record campaign-id milestone-id voter))
)

(define-read-only (get-campaign-progress (campaign-id uint))
    (match (get-campaign-milestone-config campaign-id)
        config
            (ok {
                total-milestones: (get total-milestones config),
                completed-milestones: (get completed-milestones config),
                percentage-complete: (if (> (get total-milestones config) u0)
                    (/ (* (get completed-milestones config) u100) (get total-milestones config))
                    u0
                ),
                total-allocated: (get total-allocated config),
                total-released: (get total-released config)
            })
        ERR-CAMPAIGN-NOT-FOUND
    )
)

;; Public functions

(define-public (initialize-milestone-campaign (campaign-id uint))
    (begin
        (map-set campaign-milestone-config campaign-id {
            total-milestones: u0,
            completed-milestones: u0,
            total-allocated: u0,
            total-released: u0,
            milestone-enabled: true
        })
        (ok true)
    )
)

(define-public (add-milestone 
    (campaign-id uint)
    (title (string-ascii 100))
    (description (string-utf8 500))
    (amount uint)
    (voting-duration uint)
)
    (let (
        (config (unwrap! (get-campaign-milestone-config campaign-id) ERR-CAMPAIGN-NOT-FOUND))
        (milestone-id (+ (get total-milestones config) u1))
    )
        ;; Validate
        (asserts! (> amount u0) ERR-INVALID-AMOUNT)
        (asserts! (> voting-duration u0) ERR-INVALID-VOTING-PERIOD)
        
        ;; Create milestone
        (map-set milestones 
            { campaign-id: campaign-id, milestone-id: milestone-id }
            {
                title: title,
                description: description,
                amount: amount,
                status: STATUS-PENDING,
                created-at: stacks-block-height,
                voting-deadline: (+ stacks-block-height voting-duration),
                released-at: none,
                creator: tx-sender
            }
        )
        
        ;; Update config
        (map-set campaign-milestone-config campaign-id
            (merge config {
                total-milestones: milestone-id,
                total-allocated: (+ (get total-allocated config) amount)
            })
        )
        
        (ok milestone-id)
    )
)

(define-public (start-milestone-voting (campaign-id uint) (milestone-id uint) (voting-duration uint))
    (let (
        (milestone (unwrap! (get-milestone campaign-id milestone-id) ERR-MILESTONE-NOT-FOUND))
    )
        ;; Validate
        (asserts! (is-eq tx-sender (get creator milestone)) ERR-NOT-AUTHORIZED)
        (asserts! (is-eq (get status milestone) STATUS-PENDING) ERR-INVALID-MILESTONE)
        (asserts! (> voting-duration u0) ERR-INVALID-VOTING-PERIOD)
        
        ;; Initialize voting
        (map-set milestone-votes 
            { campaign-id: campaign-id, milestone-id: milestone-id }
            {
                yes-votes: u0,
                no-votes: u0,
                total-voters: u0,
                total-voting-power: u0,
                voting-start: stacks-block-height,
                voting-end: (+ stacks-block-height voting-duration),
                approved: false
            }
        )
        
        ;; Update milestone status
        (map-set milestones 
            { campaign-id: campaign-id, milestone-id: milestone-id }
            (merge milestone { 
                status: STATUS-VOTING,
                voting-deadline: (+ stacks-block-height voting-duration)
            })
        )
        
        (ok true)
    )
)

(define-public (vote-on-milestone 
    (campaign-id uint) 
    (milestone-id uint) 
    (vote uint)
    (contribution-amount uint)
)
    (let (
        (milestone (unwrap! (get-milestone campaign-id milestone-id) ERR-MILESTONE-NOT-FOUND))
        (vote-data (unwrap! (get-milestone-votes campaign-id milestone-id) ERR-MILESTONE-NOT-FOUND))
        (voting-power (calculate-voting-power contribution-amount))
    )
        ;; Validate
        (asserts! (or (is-eq vote VOTE-YES) (is-eq vote VOTE-NO)) ERR-INVALID-MILESTONE)
        (asserts! (is-eq (get status milestone) STATUS-VOTING) ERR-VOTING-ENDED)
        (asserts! (not (has-voting-ended campaign-id milestone-id)) ERR-VOTING-ENDED)
        (asserts! (not (has-voted campaign-id milestone-id tx-sender)) ERR-ALREADY-VOTED)
        (asserts! (> contribution-amount u0) ERR-NOT-BACKER)
        
        ;; Record vote
        (map-set voter-records 
            { campaign-id: campaign-id, milestone-id: milestone-id, voter: tx-sender }
            {
                vote: vote,
                voting-power: voting-power,
                voted-at: stacks-block-height
            }
        )
        
        ;; Update voting power tracking
        (map-set campaign-voting-power
            { campaign-id: campaign-id, backer: tx-sender }
            {
                contribution-amount: contribution-amount,
                voting-power: voting-power
            }
        )
        
        ;; Update vote tallies
        (map-set milestone-votes 
            { campaign-id: campaign-id, milestone-id: milestone-id }
            (merge vote-data {
                yes-votes: (if (is-eq vote VOTE-YES) 
                    (+ (get yes-votes vote-data) voting-power) 
                    (get yes-votes vote-data)
                ),
                no-votes: (if (is-eq vote VOTE-NO) 
                    (+ (get no-votes vote-data) voting-power) 
                    (get no-votes vote-data)
                ),
                total-voters: (+ (get total-voters vote-data) u1),
                total-voting-power: (+ (get total-voting-power vote-data) voting-power)
            })
        )
        
        (ok true)
    )
)

(define-public (finalize-milestone-vote (campaign-id uint) (milestone-id uint))
    (let (
        (milestone (unwrap! (get-milestone campaign-id milestone-id) ERR-MILESTONE-NOT-FOUND))
        (vote-data (unwrap! (get-milestone-votes campaign-id milestone-id) ERR-MILESTONE-NOT-FOUND))
    )
        ;; Validate
        (asserts! (is-eq (get status milestone) STATUS-VOTING) ERR-INVALID-MILESTONE)
        (asserts! (has-voting-ended campaign-id milestone-id) ERR-VOTING-ACTIVE)
        
        ;; Determine approval
        (let (
            (approved (is-milestone-approved campaign-id milestone-id))
            (new-status (if approved STATUS-APPROVED STATUS-REJECTED))
        )
            ;; Update vote data
            (map-set milestone-votes 
                { campaign-id: campaign-id, milestone-id: milestone-id }
                (merge vote-data { approved: approved })
            )
            
            ;; Update milestone status
            (map-set milestones 
                { campaign-id: campaign-id, milestone-id: milestone-id }
                (merge milestone { status: new-status })
            )
            
            (ok approved)
        )
    )
)

(define-public (submit-milestone-deliverables 
    (campaign-id uint)
    (milestone-id uint)
    (proof-url (string-utf8 256))
    (notes (string-utf8 500))
)
    (let (
        (milestone (unwrap! (get-milestone campaign-id milestone-id) ERR-MILESTONE-NOT-FOUND))
    )
        ;; Validate
        (asserts! (is-eq tx-sender (get creator milestone)) ERR-NOT-AUTHORIZED)
        
        ;; Store deliverables
        (map-set milestone-deliverables 
            { campaign-id: campaign-id, milestone-id: milestone-id }
            {
                proof-url: proof-url,
                submitted-at: stacks-block-height,
                notes: notes
            }
        )
        
        (ok true)
    )
)

(define-public (release-milestone-funds 
    (campaign-id uint) 
    (milestone-id uint)
    (recipient principal)
)
    (let (
        (milestone (unwrap! (get-milestone campaign-id milestone-id) ERR-MILESTONE-NOT-FOUND))
        (config (unwrap! (get-campaign-milestone-config campaign-id) ERR-CAMPAIGN-NOT-FOUND))
        (amount (get amount milestone))
    )
        ;; Validate
        (asserts! (is-eq (get status milestone) STATUS-APPROVED) ERR-MILESTONE-NOT-APPROVED)
        (asserts! (>= (get total-allocated config) amount) ERR-INSUFFICIENT-FUNDS)
        
        ;; Transfer funds
        (try! (as-contract (stx-transfer? amount tx-sender recipient)))
        
        ;; Update milestone
        (map-set milestones 
            { campaign-id: campaign-id, milestone-id: milestone-id }
            (merge milestone { 
                status: STATUS-RELEASED,
                released-at: (some stacks-block-height)
            })
        )
        
        ;; Update config
        (map-set campaign-milestone-config campaign-id
            (merge config {
                completed-milestones: (+ (get completed-milestones config) u1),
                total-released: (+ (get total-released config) amount)
            })
        )
        
        (ok amount)
    )
)

(define-public (cancel-milestone (campaign-id uint) (milestone-id uint))
    (let (
        (milestone (unwrap! (get-milestone campaign-id milestone-id) ERR-MILESTONE-NOT-FOUND))
    )
        ;; Validate
        (asserts! (is-eq tx-sender (get creator milestone)) ERR-NOT-AUTHORIZED)
        (asserts! (or 
            (is-eq (get status milestone) STATUS-PENDING)
            (is-eq (get status milestone) STATUS-REJECTED)
        ) ERR-INVALID-MILESTONE)
        
        ;; Update status
        (map-set milestones 
            { campaign-id: campaign-id, milestone-id: milestone-id }
            (merge milestone { status: STATUS-CANCELLED })
        )
        
        (ok true)
    )
)

(define-public (update-milestone 
    (campaign-id uint)
    (milestone-id uint)
    (title (string-ascii 100))
    (description (string-utf8 500))
)
    (let (
        (milestone (unwrap! (get-milestone campaign-id milestone-id) ERR-MILESTONE-NOT-FOUND))
    )
        ;; Validate
        (asserts! (is-eq tx-sender (get creator milestone)) ERR-NOT-AUTHORIZED)
        (asserts! (is-eq (get status milestone) STATUS-PENDING) ERR-INVALID-MILESTONE)
        
        ;; Update milestone
        (map-set milestones 
            { campaign-id: campaign-id, milestone-id: milestone-id }
            (merge milestone { 
                title: title,
                description: description
            })
        )
        
        (ok true)
    )
)

Functions (21)

FunctionAccessArgs
calculate-voting-powerprivatecontribution-amount: uint
has-voting-endedprivatecampaign-id: uint, milestone-id: uint
calculate-approval-percentageprivateyes-votes: uint, total-voting-power: uint
is-milestone-approvedprivatecampaign-id: uint, milestone-id: uint
get-campaign-milestone-configread-onlycampaign-id: uint
get-milestoneread-onlycampaign-id: uint, milestone-id: uint
get-milestone-votesread-onlycampaign-id: uint, milestone-id: uint
get-voter-recordread-onlycampaign-id: uint, milestone-id: uint, voter: principal
get-milestone-deliverablesread-onlycampaign-id: uint, milestone-id: uint
get-voting-powerread-onlycampaign-id: uint, backer: principal
get-milestone-statusread-onlycampaign-id: uint, milestone-id: uint
get-voting-resultsread-onlycampaign-id: uint, milestone-id: uint
has-votedread-onlycampaign-id: uint, milestone-id: uint, voter: principal
get-campaign-progressread-onlycampaign-id: uint
initialize-milestone-campaignpubliccampaign-id: uint
add-milestonepubliccampaign-id: uint, title: (string-ascii 100
start-milestone-votingpubliccampaign-id: uint, milestone-id: uint, voting-duration: uint
finalize-milestone-votepubliccampaign-id: uint, milestone-id: uint
submit-milestone-deliverablespubliccampaign-id: uint, milestone-id: uint, proof-url: (string-utf8 256
cancel-milestonepubliccampaign-id: uint, milestone-id: uint
update-milestonepubliccampaign-id: uint, milestone-id: uint, title: (string-ascii 100