diff --git a/research-chain/README.md b/research-chain/README.md new file mode 100644 index 0000000..dc773cf --- /dev/null +++ b/research-chain/README.md @@ -0,0 +1,123 @@ +# ResearchChain Smart Contract + +## Overview + +ResearchChain is a decentralized scientific research funding platform implemented as a smart contract on the Stacks blockchain. It allows researchers to submit proposals, reviewers to evaluate them, and funders to support approved projects. The contract also includes a refund mechanism for certain scenarios. + +## Features + +- Proposal submission by researchers +- Review and voting system for quality control +- Funding mechanism for approved proposals +- Withdrawal process for funded projects +- Refund mechanism for eligible proposals +- Role-based access control (researchers, reviewers, funders, contract owner) +- Configurable voting and funding parameters + +## Contract Structure + +The contract consists of several key components: + +1. **Proposals**: Researchers can submit proposals for funding. +2. **Review Process**: Authorized reviewers can vote on proposals. +3. **Funding**: Approved proposals can receive funding from supporters. +4. **Withdrawal**: Researchers can withdraw funds once their proposal is fully funded. +5. **Refunds**: Funders can request refunds under certain conditions. + +## Proposal Lifecycle + +1. **Submitted**: A researcher submits a new proposal. +2. **Under Review**: The contract owner moves the proposal to the review phase. +3. **Approved/Rejected**: Based on reviewer votes, the proposal is either approved or rejected. +4. **Open for Funding**: Approved proposals can be opened for funding by the contract owner. +5. **Funded**: The proposal reaches its funding goal. +6. **Closed**: The researcher withdraws the funds, and the proposal is closed. +7. **Refundable**: If a proposal doesn't meet its funding goal within the deadline or is closed, it becomes eligible for refunds. + +## Key Functions + +### For Researchers + +- `submit-proposal`: Submit a new research proposal. +- `withdraw-funds`: Withdraw funds for a fully funded proposal. + +### For Reviewers + +- `vote-on-proposal`: Vote to approve or reject a proposal under review. + +### For Funders + +- `fund-proposal`: Contribute funds to an approved and open proposal. +- `request-refund`: Request a refund for an eligible proposal. + +### For Contract Owner + +- `update-proposal-status`: Update the status of a proposal. +- `add-reviewer`: Add a new reviewer to the system. +- `remove-reviewer`: Remove a reviewer from the system. +- `set-required-votes`: Set the number of votes required for a proposal decision. +- `set-funding-period`: Set the funding period for proposals. + +## How to Use + +1. **Submitting a Proposal**: + Researchers call `submit-proposal` with their proposal details. + +2. **Reviewing a Proposal**: + - The contract owner updates the proposal status to "Under Review". + - Authorized reviewers call `vote-on-proposal` to cast their votes. + - Once the required number of votes is reached, the proposal is automatically approved or rejected. + +3. **Funding a Proposal**: + - The contract owner updates approved proposals to "Open" status. + - Funders can call `fund-proposal` to contribute to open proposals. + +4. **Withdrawing Funds**: + Once a proposal is fully funded, the researcher can call `withdraw-funds` to receive the funds. + +5. **Requesting Refunds**: + If a proposal doesn't meet its funding goal within the deadline or is closed, funders can call `request-refund` to get their contributions back. + +## Error Codes + +- `ERR_NOT_AUTHORIZED (u100)`: User not authorized for the action. +- `ERR_INVALID_AMOUNT (u101)`: Invalid amount specified. +- `ERR_PROPOSAL_NOT_FOUND (u102)`: Proposal ID not found. +- `ERR_ALREADY_FUNDED (u103)`: Proposal already fully funded. +- `ERR_INVALID_TITLE (u104)`: Invalid proposal title. +- `ERR_INVALID_DESCRIPTION (u105)`: Invalid proposal description. +- `ERR_INVALID_FUNDING_GOAL (u106)`: Invalid funding goal. +- `ERR_INVALID_PROPOSAL_ID (u107)`: Invalid proposal ID. +- `ERR_INVALID_STATUS (u108)`: Invalid proposal status. +- `ERR_ALREADY_VOTED (u109)`: Reviewer has already voted on the proposal. +- `ERR_NOT_REVIEWER (u110)`: User is not an authorized reviewer. +- `ERR_INVALID_REVIEWER (u111)`: Invalid reviewer specified. +- `ERR_NOT_FUNDER (u112)`: User is not a funder of the proposal. +- `ERR_REFUND_NOT_AVAILABLE (u113)`: Refund is not available for the proposal. + +## Security Considerations + +- Only the contract owner can add or remove reviewers and update proposal statuses. +- Reviewers can only vote once per proposal. +- Funds are locked in the contract until the proposal is fully funded and the researcher withdraws them, or until a refund is requested. +- Input validation is performed on all public functions to ensure data integrity. +- The contract uses a deadline mechanism to determine when proposals become eligible for refunds. + +## Read-Only Functions + +- `get-proposal`: Retrieve details of a specific proposal. +- `get-proposal-count`: Get the total number of proposals. +- `get-funding`: Get the funding amount for a specific proposal and funder. +- `get-proposal-status`: Retrieve the status of a specific proposal. +- `is-active-reviewer`: Check if an account is an active reviewer. +- `get-required-votes`: Get the current number of required votes for proposal decisions. +- `get-funding-period`: Get the current funding period for proposals. +- `check-refund-eligibility`: Check if a proposal is eligible for refunds. + +## Disclaimer + +This smart contract is provided as-is. Users should review and understand the code before interacting with it on the blockchain. Always test thoroughly on a testnet before deploying to mainnet. + +## Author + +Blessing Eze \ No newline at end of file diff --git a/research-chain/contracts/sci-bloom.clar b/research-chain/contracts/sci-bloom.clar index 490760b..59cd42d 100644 --- a/research-chain/contracts/sci-bloom.clar +++ b/research-chain/contracts/sci-bloom.clar @@ -1,5 +1,5 @@ -;; ScienceBloom: Decentralized Scientific Research Funding -;; This contract allows researchers to submit proposals and receive funding +;; ResearchChain: Decentralized Scientific Research Funding +;; This contract allows researchers to submit proposals, reviewers to vote, funders to support approved projects, and implement a refund mechanism ;; Define constants (define-constant CONTRACT_OWNER tx-sender) @@ -7,6 +7,30 @@ (define-constant ERR_INVALID_AMOUNT (err u101)) (define-constant ERR_PROPOSAL_NOT_FOUND (err u102)) (define-constant ERR_ALREADY_FUNDED (err u103)) +(define-constant ERR_INVALID_TITLE (err u104)) +(define-constant ERR_INVALID_DESCRIPTION (err u105)) +(define-constant ERR_INVALID_FUNDING_GOAL (err u106)) +(define-constant ERR_INVALID_PROPOSAL_ID (err u107)) +(define-constant ERR_INVALID_STATUS (err u108)) +(define-constant ERR_ALREADY_VOTED (err u109)) +(define-constant ERR_NOT_REVIEWER (err u110)) +(define-constant ERR_INVALID_REVIEWER (err u111)) +(define-constant ERR_NOT_FUNDER (err u112)) +(define-constant ERR_REFUND_NOT_AVAILABLE (err u113)) + +;; Define proposal statuses +(define-constant STATUS_SUBMITTED u0) +(define-constant STATUS_UNDER_REVIEW u1) +(define-constant STATUS_APPROVED u2) +(define-constant STATUS_REJECTED u3) +(define-constant STATUS_OPEN u4) +(define-constant STATUS_FUNDED u5) +(define-constant STATUS_CLOSED u6) +(define-constant STATUS_REFUNDABLE u7) + +;; Define vote options +(define-constant VOTE_APPROVE u1) +(define-constant VOTE_REJECT u0) ;; Define data maps (define-map proposals @@ -17,7 +41,10 @@ description: (string-ascii 1000), funding-goal: uint, current-funding: uint, - is-active: bool + status: uint, + approve-votes: uint, + reject-votes: uint, + deadline: uint } ) @@ -26,69 +53,265 @@ { amount: uint } ) +(define-map reviewers + { reviewer: principal } + { is-active: bool } +) + +(define-map votes + { proposal-id: uint, reviewer: principal } + { vote: uint } +) + ;; Define variables (define-data-var proposal-counter uint u0) +(define-data-var required-votes uint u3) +(define-data-var funding-period uint u43200) ;; Default to 30 days (in blocks, assuming 1 block every 60 seconds) + +;; Helper functions for input validation +(define-private (is-valid-title (title (string-ascii 100))) + (and (> (len title) u0) (<= (len title) u100)) +) + +(define-private (is-valid-description (description (string-ascii 1000))) + (and (> (len description) u0) (<= (len description) u1000)) +) + +(define-private (is-valid-funding-goal (funding-goal uint)) + (> funding-goal u0) +) + +(define-private (is-valid-proposal-id (proposal-id uint)) + (<= proposal-id (var-get proposal-counter)) +) + +(define-private (is-valid-status (status uint)) + (and (>= status STATUS_SUBMITTED) (<= status STATUS_REFUNDABLE)) +) + +(define-private (is-reviewer (account principal)) + (default-to false (get is-active (map-get? reviewers { reviewer: account }))) +) ;; Public functions ;; Submit a new research proposal (define-public (submit-proposal (title (string-ascii 100)) (description (string-ascii 1000)) (funding-goal uint)) - (let - ( - (proposal-id (+ (var-get proposal-counter) u1)) + (begin + (asserts! (is-valid-title title) ERR_INVALID_TITLE) + (asserts! (is-valid-description description) ERR_INVALID_DESCRIPTION) + (asserts! (is-valid-funding-goal funding-goal) ERR_INVALID_FUNDING_GOAL) + (let + ( + (proposal-id (+ (var-get proposal-counter) u1)) + (deadline (+ block-height (var-get funding-period))) + ) + (map-set proposals + { proposal-id: proposal-id } + { + researcher: tx-sender, + title: title, + description: description, + funding-goal: funding-goal, + current-funding: u0, + status: STATUS_SUBMITTED, + approve-votes: u0, + reject-votes: u0, + deadline: deadline + } + ) + (var-set proposal-counter proposal-id) + (ok proposal-id) ) - (map-set proposals - { proposal-id: proposal-id } - { - researcher: tx-sender, - title: title, - description: description, - funding-goal: funding-goal, - current-funding: u0, - is-active: true - } + ) +) + +;; Vote on a proposal (only for reviewers) +(define-public (vote-on-proposal (proposal-id uint) (vote uint)) + (begin + (asserts! (is-reviewer tx-sender) ERR_NOT_REVIEWER) + (asserts! (is-valid-proposal-id proposal-id) ERR_INVALID_PROPOSAL_ID) + (asserts! (or (is-eq vote VOTE_APPROVE) (is-eq vote VOTE_REJECT)) ERR_INVALID_STATUS) + (let + ( + (proposal (unwrap-panic (map-get? proposals { proposal-id: proposal-id }))) + (existing-vote (map-get? votes { proposal-id: proposal-id, reviewer: tx-sender })) + ) + (asserts! (is-eq (get status proposal) STATUS_UNDER_REVIEW) ERR_INVALID_STATUS) + (asserts! (is-none existing-vote) ERR_ALREADY_VOTED) + (map-set votes { proposal-id: proposal-id, reviewer: tx-sender } { vote: vote }) + (if (is-eq vote VOTE_APPROVE) + (map-set proposals { proposal-id: proposal-id } + (merge proposal { approve-votes: (+ (get approve-votes proposal) u1) })) + (map-set proposals { proposal-id: proposal-id } + (merge proposal { reject-votes: (+ (get reject-votes proposal) u1) })) + ) + (let + ( + (updated-proposal (unwrap-panic (map-get? proposals { proposal-id: proposal-id }))) + (total-votes (+ (get approve-votes updated-proposal) (get reject-votes updated-proposal))) + ) + (if (>= total-votes (var-get required-votes)) + (if (> (get approve-votes updated-proposal) (get reject-votes updated-proposal)) + (map-set proposals { proposal-id: proposal-id } + (merge updated-proposal { status: STATUS_APPROVED })) + (map-set proposals { proposal-id: proposal-id } + (merge updated-proposal { status: STATUS_REJECTED })) + ) + true + ) + ) + (ok true) ) - (var-set proposal-counter proposal-id) - (ok proposal-id) ) ) ;; Fund a research proposal (define-public (fund-proposal (proposal-id uint) (amount uint)) - (let - ( - (proposal (unwrap! (map-get? proposals { proposal-id: proposal-id }) (err ERR_PROPOSAL_NOT_FOUND))) - (new-funding (+ (get current-funding proposal) amount)) - ) + (begin + (asserts! (is-valid-proposal-id proposal-id) ERR_INVALID_PROPOSAL_ID) (asserts! (> amount u0) ERR_INVALID_AMOUNT) - (asserts! (get is-active proposal) ERR_ALREADY_FUNDED) - (try! (stx-transfer? amount tx-sender (as-contract tx-sender))) - (map-set proposals - { proposal-id: proposal-id } - (merge proposal { current-funding: new-funding, is-active: (< new-funding (get funding-goal proposal)) }) + (let + ( + (proposal (unwrap-panic (map-get? proposals { proposal-id: proposal-id }))) + (new-funding (+ (get current-funding proposal) amount)) + ) + (asserts! (is-eq (get status proposal) STATUS_OPEN) ERR_INVALID_STATUS) + (asserts! (<= new-funding (get funding-goal proposal)) ERR_INVALID_AMOUNT) + (try! (stx-transfer? amount tx-sender (as-contract tx-sender))) + (map-set proposals + { proposal-id: proposal-id } + (merge proposal { + current-funding: new-funding, + status: (if (is-eq new-funding (get funding-goal proposal)) STATUS_FUNDED STATUS_OPEN) + }) + ) + (map-set fundings + { proposal-id: proposal-id, funder: tx-sender } + { amount: (+ amount (default-to u0 (get amount (map-get? fundings { proposal-id: proposal-id, funder: tx-sender })))) } + ) + (ok true) ) - (map-set fundings - { proposal-id: proposal-id, funder: tx-sender } - { amount: (default-to u0 (get amount (map-get? fundings { proposal-id: proposal-id, funder: tx-sender }))) } - ) - (ok true) ) ) ;; Withdraw funds for a fully funded proposal (only by the researcher) (define-public (withdraw-funds (proposal-id uint)) + (begin + (asserts! (is-valid-proposal-id proposal-id) ERR_INVALID_PROPOSAL_ID) + (let + ( + (proposal (unwrap-panic (map-get? proposals { proposal-id: proposal-id }))) + ) + (asserts! (is-eq (get researcher proposal) tx-sender) ERR_NOT_AUTHORIZED) + (asserts! (is-eq (get status proposal) STATUS_FUNDED) ERR_INVALID_STATUS) + (try! (as-contract (stx-transfer? (get current-funding proposal) tx-sender (get researcher proposal)))) + (map-set proposals + { proposal-id: proposal-id } + (merge proposal { current-funding: u0, status: STATUS_CLOSED }) + ) + (ok true) + ) + ) +) + +;; Update proposal status (only by CONTRACT_OWNER) +(define-public (update-proposal-status (proposal-id uint) (new-status uint)) + (begin + (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED) + (asserts! (is-valid-proposal-id proposal-id) ERR_INVALID_PROPOSAL_ID) + (asserts! (is-valid-status new-status) ERR_INVALID_STATUS) + (let + ( + (proposal (unwrap-panic (map-get? proposals { proposal-id: proposal-id }))) + ) + (map-set proposals + { proposal-id: proposal-id } + (merge proposal { status: new-status }) + ) + (ok true) + ) + ) +) + +;; Add a reviewer (only by CONTRACT_OWNER) +(define-public (add-reviewer (reviewer principal)) + (begin + (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED) + (asserts! (not (is-eq reviewer CONTRACT_OWNER)) ERR_INVALID_REVIEWER) + (map-set reviewers { reviewer: reviewer } { is-active: true }) + (ok true) + ) +) + +;; Remove a reviewer (only by CONTRACT_OWNER) +(define-public (remove-reviewer (reviewer principal)) + (begin + (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED) + (asserts! (not (is-eq reviewer CONTRACT_OWNER)) ERR_INVALID_REVIEWER) + (map-delete reviewers { reviewer: reviewer }) + (ok true) + ) +) + +;; Set required votes (only by CONTRACT_OWNER) +(define-public (set-required-votes (new-required-votes uint)) + (begin + (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED) + (asserts! (> new-required-votes u0) ERR_INVALID_AMOUNT) + (var-set required-votes new-required-votes) + (ok true) + ) +) + +;; Set funding period (only by CONTRACT_OWNER) +(define-public (set-funding-period (new-funding-period uint)) + (begin + (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED) + (asserts! (> new-funding-period u0) ERR_INVALID_AMOUNT) + (var-set funding-period new-funding-period) + (ok true) + ) +) + +;; Check if a proposal is eligible for refund +(define-private (is-refund-eligible (proposal { proposal-id: uint })) (let ( - (proposal (unwrap! (map-get? proposals { proposal-id: proposal-id }) (err ERR_PROPOSAL_NOT_FOUND))) + (proposal-data (unwrap-panic (map-get? proposals proposal))) ) - (asserts! (is-eq (get researcher proposal) tx-sender) ERR_NOT_AUTHORIZED) - (asserts! (>= (get current-funding proposal) (get funding-goal proposal)) ERR_INVALID_AMOUNT) - (try! (as-contract (stx-transfer? (get current-funding proposal) tx-sender (get researcher proposal)))) - (map-set proposals - { proposal-id: proposal-id } - (merge proposal { current-funding: u0, is-active: false }) + (or + (and + (< (get current-funding proposal-data) (get funding-goal proposal-data)) + (> block-height (get deadline proposal-data)) + ) + (is-eq (get status proposal-data) STATUS_CLOSED) + ) + ) +) + +;; Request a refund for a proposal +(define-public (request-refund (proposal-id uint)) + (begin + (asserts! (is-valid-proposal-id proposal-id) ERR_INVALID_PROPOSAL_ID) + (let + ( + (proposal (unwrap-panic (map-get? proposals { proposal-id: proposal-id }))) + (funding (unwrap-panic (map-get? fundings { proposal-id: proposal-id, funder: tx-sender }))) + ) + (asserts! (is-refund-eligible { proposal-id: proposal-id }) ERR_REFUND_NOT_AVAILABLE) + (asserts! (> (get amount funding) u0) ERR_NOT_FUNDER) + (try! (as-contract (stx-transfer? (get amount funding) tx-sender tx-sender))) + (map-delete fundings { proposal-id: proposal-id, funder: tx-sender }) + (map-set proposals + { proposal-id: proposal-id } + (merge proposal { + current-funding: (- (get current-funding proposal) (get amount funding)), + status: STATUS_REFUNDABLE + }) + ) + (ok true) ) - (ok true) ) ) @@ -107,4 +330,29 @@ ;; Get funding amount for a specific proposal and funder (define-read-only (get-funding (proposal-id uint) (funder principal)) (map-get? fundings { proposal-id: proposal-id, funder: funder }) +) + +;; Get proposal status +(define-read-only (get-proposal-status (proposal-id uint)) + (get status (unwrap-panic (map-get? proposals { proposal-id: proposal-id }))) +) + +;; Check if an account is a reviewer +(define-read-only (is-active-reviewer (account principal)) + (is-reviewer account) +) + +;; Get required votes +(define-read-only (get-required-votes) + (var-get required-votes) +) + +;; Get funding period +(define-read-only (get-funding-period) + (var-get funding-period) +) + +;; Check if a proposal is eligible for refund +(define-read-only (check-refund-eligibility (proposal-id uint)) + (is-refund-eligible { proposal-id: proposal-id }) ) \ No newline at end of file