Source Code


;; title: openscienceX
;; version: 1.0.0
;; summary: Decentralized scientific funding platform
;; description: Allows researchers to submit proposals, community to vote and fund them, with milestone-based releases and Impact NFTs.

;; traits
;; implementation of SIP-009 NFT trait would go here if provided externally
;; (impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)

;; token definitions
(define-non-fungible-token impact-nft uint)

;; constants
(define-constant CONTRACT-OWNER tx-sender)
(define-constant ERR-NOT-AUTHORIZED (err u100))
(define-constant ERR-NOT-FOUND (err u101))
(define-constant ERR-ALREADY-EXISTS (err u102))
(define-constant ERR-INVALID-STATUS (err u103))
(define-constant ERR-INSUFFICIENT-FUNDS (err u104))
(define-constant ERR-VOTING-ENDED (err u105))
(define-constant ERR-MILESTONE-NOT-APPROVED (err u106))
(define-constant ERR-PROPOSAL-NOT-FUNDED (err u107))

;; Status Constants
(define-constant STATUS-PROPOSED "PROPOSED")
(define-constant STATUS-VOTING "VOTING")
(define-constant STATUS-FUNDED "FUNDED")
(define-constant STATUS-REJECTED "REJECTED")
(define-constant STATUS-COMPLETED "COMPLETED")

;; data vars
(define-data-var proposal-nonce uint u0)
(define-data-var milestone-nonce uint u0)
(define-data-var contribution-nonce uint u0)
(define-data-var nft-nonce uint u0)
(define-data-var test-counter uint u0) ;; For testing purposes

;; data maps
(define-map proposals
    uint
    {
        researcher: principal,
        title: (string-utf8 100),
        abstract: (string-utf8 500),
        category: (string-ascii 50),
        funding-requested: uint,
        funding-received: uint,
        status: (string-ascii 20),
        votes-for: uint,
        votes-against: uint,
        created-at: uint,
        voting-ends: uint
    }
)

(define-map milestones
    uint
    {
        proposal-id: uint,
        title: (string-utf8 100),
        funding-amount: uint,
        status: (string-ascii 20), ;; PENDING, APPROVED, REJECTED
        deliverable-hash: (optional (buff 32)),
        completed-at: (optional uint)
    }
)

(define-map contributions
    uint
    {
        donor: principal,
        proposal-id: uint,
        amount: uint,
        nft-id: uint, ;; 0 if not minted yet
        timestamp: uint
    }
)

(define-map researcher-profiles
    principal
    {
        name: (string-utf8 50),
        institution: (string-utf8 100),
        is-verified: bool,
        reputation-score: uint
    }
)

(define-map nft-metadata
    uint
    {
        owner: principal,
        token-uri: (string-ascii 256)
    }
)

;; private functions (Counter Logic)

(define-private (generate-proposal-id)
    (let
        (
            (current-id (+ (var-get proposal-nonce) u1))
        )
        (var-set proposal-nonce current-id)
        current-id
    )
)

(define-private (generate-milestone-id)
    (let
        (
            (current-id (+ (var-get milestone-nonce) u1))
        )
        (var-set milestone-nonce current-id)
        current-id
    )
)

(define-private (generate-contribution-id)
    (let
        (
            (current-id (+ (var-get contribution-nonce) u1))
        )
        (var-set contribution-nonce current-id)
        current-id
    )
)

(define-private (generate-nft-id)
    (let
        (
            (current-id (+ (var-get nft-nonce) u1))
        )
        (var-set nft-nonce current-id)
        current-id
    )
)

;; public functions

;; --------------------------------------------------------------------------
;; Test Counter Logic (Public)
;; --------------------------------------------------------------------------

(define-public (utility-increment-counter)
    (begin
        (var-set test-counter (+ (var-get test-counter) u1))
        (ok (var-get test-counter))
    )
)

(define-public (utility-decrement-counter)
    (begin
        (let ((current (var-get test-counter)))
            (if (> current u0)
                (begin
                    (var-set test-counter (- current u1))
                    (ok (var-get test-counter))
                )
                (err u0) ;; Cannot decrement below 0
            ) 
        )
    )
)

(define-read-only (utility-get-counter)
    (ok (var-get test-counter))
)

;; --------------------------------------------------------------------------
;; Researcher Management
;; --------------------------------------------------------------------------

(define-public (register-researcher (name (string-utf8 50)) (institution (string-utf8 100)))
    (begin
        (asserts! (is-none (map-get? researcher-profiles tx-sender)) ERR-ALREADY-EXISTS)
        (ok (map-set researcher-profiles tx-sender {
            name: name,
            institution: institution,
            is-verified: false,
            reputation-score: u0
        }))
    )
)

(define-read-only (get-researcher-profile (researcher principal))
    (map-get? researcher-profiles researcher)
)

;; --------------------------------------------------------------------------
;; Proposal Management
;; --------------------------------------------------------------------------

(define-public (submit-proposal (title (string-utf8 100)) (abstract (string-utf8 500)) (category (string-ascii 50)) (funding-requested uint))
    (let
        (
            (new-id (generate-proposal-id))
        )
        (map-set proposals new-id {
            researcher: tx-sender,
            title: title,
            abstract: abstract,
            category: category,
            funding-requested: funding-requested,
            funding-received: u0,
            status: STATUS-PROPOSED,
            votes-for: u0,
            votes-against: u0,
            created-at: block-height,
            voting-ends: (+ block-height u144) ;; Approx 1 day for simplicity / example
        })
        (ok new-id)
    )
)

(define-read-only (get-proposal (proposal-id uint))
    (map-get? proposals proposal-id)
)

;; --------------------------------------------------------------------------
;; Voting
;; --------------------------------------------------------------------------

(define-public (vote-on-proposal (proposal-id uint) (vote bool))
    (let
        (
            (proposal (unwrap! (map-get? proposals proposal-id) ERR-NOT-FOUND))
        )
        ;; In a real quadratic voting system, we would track user votes individually
        ;; For this simplified version, we just increment based on sender
        ;; Simplification: One vote per call, no cost for now (requires token locking in full spec)
        (begin
            (asserts! (< block-height (get voting-ends proposal)) ERR-VOTING-ENDED)
            
            (map-set proposals proposal-id
                (merge proposal {
                    votes-for: (if vote (+ (get votes-for proposal) u1) (get votes-for proposal)),
                    votes-against: (if vote (get votes-against proposal) (+ (get votes-against proposal) u1))
                })
            )
            (ok true)
        )
    )
)

;; --------------------------------------------------------------------------
;; Funding
;; --------------------------------------------------------------------------

(define-public (contribute (proposal-id uint) (amount uint))
    (let
        (
            (proposal (unwrap! (map-get? proposals proposal-id) ERR-NOT-FOUND))
            (contribution-id (generate-contribution-id))
        )
        (begin
            (try! (stx-transfer? amount tx-sender (as-contract tx-sender)))
            
            ;; Mint NFT
            (let ((nft-id (generate-nft-id)))
                (begin
                    (try! (nft-mint? impact-nft nft-id tx-sender))
                    (map-set nft-metadata nft-id {
                        owner: tx-sender,
                        token-uri: "https://opensciencex.io/metadata/impact-nft.json"
                    })
                    
                    (map-set contributions contribution-id {
                        donor: tx-sender,
                        proposal-id: proposal-id,
                        amount: amount,
                        nft-id: nft-id,
                        timestamp: block-height
                    })
                    
                    (map-set proposals proposal-id
                        (merge proposal {
                            funding-received: (+ (get funding-received proposal) amount),
                            status: (if (>= (+ (get funding-received proposal) amount) (get funding-requested proposal)) STATUS-FUNDED (get status proposal))
                        })
                    )
                    (ok contribution-id)
                )
            )
        )
    )
)

;; --------------------------------------------------------------------------
;; Milestones
;; --------------------------------------------------------------------------

(define-public (add-milestone (proposal-id uint) (title (string-utf8 100)) (funding-amount uint))
    (let
        (
            (proposal (unwrap! (map-get? proposals proposal-id) ERR-NOT-FOUND))
            (milestone-id (generate-milestone-id))
        )
        (begin
            (asserts! (is-eq tx-sender (get researcher proposal)) ERR-NOT-AUTHORIZED)
            (map-set milestones milestone-id {
                proposal-id: proposal-id,
                title: title,
                funding-amount: funding-amount,
                status: "PENDING",
                deliverable-hash: none,
                completed-at: none
            })
            (ok milestone-id)
        )
    )
)

(define-public (submit-milestone-deliverable (milestone-id uint) (deliverable-hash (buff 32)))
    (let
        (
            (milestone (unwrap! (map-get? milestones milestone-id) ERR-NOT-FOUND))
            (proposal (unwrap! (map-get? proposals (get proposal-id milestone)) ERR-NOT-FOUND))
        )
        (begin
            (asserts! (is-eq tx-sender (get researcher proposal)) ERR-NOT-AUTHORIZED)
            (map-set milestones milestone-id
                (merge milestone {
                    deliverable-hash: (some deliverable-hash),
                    status: "SUBMITTED"
                })
            )
            (ok true)
        )
    )
)

(define-public (approve-milestone (milestone-id uint))
    (let
        (
            (milestone (unwrap! (map-get? milestones milestone-id) ERR-NOT-FOUND))
            (proposal (unwrap! (map-get? proposals (get proposal-id milestone)) ERR-NOT-FOUND))
            (amount (get funding-amount milestone))
            (researcher (get researcher proposal))
        )
        (begin
            ;; Simplified: Anyone can approve for now (in reality, should be governance or specific voters)
            (asserts! (is-eq (get status milestone) "SUBMITTED") ERR-INVALID-STATUS)
            
            ;; Release funds
            ;; We use as-contract to switch context to the contract principal
            ;; checks checks if the contract has enough funds and sends to researcher
            (try! (as-contract (stx-transfer? amount tx-sender researcher)))
            
            (map-set milestones milestone-id
                (merge milestone {
                    status: "APPROVED",
                    completed-at: (some block-height)
                })
            )
            (ok true)
        )
    )
)


;; --------------------------------------------------------------------------
;; NFT Standard Functions (SIP-009)
;; --------------------------------------------------------------------------

(define-read-only (get-last-token-id)
    (ok (var-get nft-nonce))
)

(define-read-only (get-token-uri (token-id uint))
    (ok (get token-uri (map-get? nft-metadata token-id)))
)

(define-read-only (get-owner (token-id uint))
    (ok (nft-get-owner? impact-nft token-id))
)

(define-public (transfer (token-id uint) (sender principal) (recipient principal))
    (begin
        (asserts! (is-eq tx-sender sender) ERR-NOT-AUTHORIZED)
        (try! (nft-transfer? impact-nft token-id sender recipient))
        ;; Update internal map if needed? No, built-in map handles ownership, metadata map is fine.
        ;; However, strict consistent metadata map owner update:
        (let ((metadata (unwrap! (map-get? nft-metadata token-id) ERR-NOT-FOUND)))
             (map-set nft-metadata token-id (merge metadata { owner: recipient }))
        )
        (ok true)
    )
)

Functions (20)

FunctionAccessArgs
generate-proposal-idprivate
generate-milestone-idprivate
generate-contribution-idprivate
generate-nft-idprivate
utility-increment-counterpublic
utility-decrement-counterpublic
utility-get-counterread-only
register-researcherpublicname: (string-utf8 50
get-researcher-profileread-onlyresearcher: principal
submit-proposalpublictitle: (string-utf8 100
get-proposalread-onlyproposal-id: uint
vote-on-proposalpublicproposal-id: uint, vote: bool
contributepublicproposal-id: uint, amount: uint
add-milestonepublicproposal-id: uint, title: (string-utf8 100
submit-milestone-deliverablepublicmilestone-id: uint, deliverable-hash: (buff 32
approve-milestonepublicmilestone-id: uint
get-last-token-idread-only
get-token-uriread-onlytoken-id: uint
get-ownerread-onlytoken-id: uint
transferpublictoken-id: uint, sender: principal, recipient: principal