From 2ec24651c41a87a83476c26c4a7aad37a3e6c0d9 Mon Sep 17 00:00:00 2001 From: Vicky08100 Date: Thu, 12 Dec 2024 16:12:54 +0100 Subject: [PATCH 1/3] Added Readme file and Open Source Project Funding Contract --- project-crowdfunding/.gitattributes | 3 + project-crowdfunding/.gitignore | 13 ++ project-crowdfunding/.vscode/settings.json | 4 + project-crowdfunding/.vscode/tasks.json | 19 ++ project-crowdfunding/Clarinet.toml | 19 ++ project-crowdfunding/README.md | 211 ++++++++++++++++++ project-crowdfunding/Testnet.toml | 7 + .../contracts/stx-project-funding.clar | 202 +++++++++++++++++ project-crowdfunding/package.json | 24 ++ project-crowdfunding/settings/Devnet.toml | 151 +++++++++++++ .../tests/stx-project-funding.test.ts | 21 ++ project-crowdfunding/tsconfig.json | 26 +++ project-crowdfunding/vitest.config.js | 42 ++++ 13 files changed, 742 insertions(+) create mode 100644 project-crowdfunding/.gitattributes create mode 100644 project-crowdfunding/.gitignore create mode 100644 project-crowdfunding/.vscode/settings.json create mode 100644 project-crowdfunding/.vscode/tasks.json create mode 100644 project-crowdfunding/Clarinet.toml create mode 100644 project-crowdfunding/README.md create mode 100644 project-crowdfunding/Testnet.toml create mode 100644 project-crowdfunding/contracts/stx-project-funding.clar create mode 100644 project-crowdfunding/package.json create mode 100644 project-crowdfunding/settings/Devnet.toml create mode 100644 project-crowdfunding/tests/stx-project-funding.test.ts create mode 100644 project-crowdfunding/tsconfig.json create mode 100644 project-crowdfunding/vitest.config.js diff --git a/project-crowdfunding/.gitattributes b/project-crowdfunding/.gitattributes new file mode 100644 index 0000000..da6a065 --- /dev/null +++ b/project-crowdfunding/.gitattributes @@ -0,0 +1,3 @@ +tests/** linguist-vendored +vitest.config.js linguist-vendored +* text=lf diff --git a/project-crowdfunding/.gitignore b/project-crowdfunding/.gitignore new file mode 100644 index 0000000..76c2842 --- /dev/null +++ b/project-crowdfunding/.gitignore @@ -0,0 +1,13 @@ + +**/settings/Mainnet.toml +**/settings/Testnet.toml +.cache/** +history.txt + +logs +*.log +npm-debug.log* +coverage +*.info +costs-reports.json +node_modules diff --git a/project-crowdfunding/.vscode/settings.json b/project-crowdfunding/.vscode/settings.json new file mode 100644 index 0000000..3062519 --- /dev/null +++ b/project-crowdfunding/.vscode/settings.json @@ -0,0 +1,4 @@ + +{ + "files.eol": "\n" +} diff --git a/project-crowdfunding/.vscode/tasks.json b/project-crowdfunding/.vscode/tasks.json new file mode 100644 index 0000000..4dec0ff --- /dev/null +++ b/project-crowdfunding/.vscode/tasks.json @@ -0,0 +1,19 @@ + +{ + "version": "2.0.0", + "tasks": [ + { + "label": "check contracts", + "group": "test", + "type": "shell", + "command": "clarinet check" + }, + { + "type": "npm", + "script": "test", + "group": "test", + "problemMatcher": [], + "label": "npm test" + } + ] +} diff --git a/project-crowdfunding/Clarinet.toml b/project-crowdfunding/Clarinet.toml new file mode 100644 index 0000000..e4706e7 --- /dev/null +++ b/project-crowdfunding/Clarinet.toml @@ -0,0 +1,19 @@ +[project] +name = 'project-crowdfunding' +description = '' +authors = [] +telemetry = true +cache_dir = '.\.cache' +requirements = [] +[contracts.stx-project-funding] +path = 'contracts/stx-project-funding.clar' +clarity_version = 2 +epoch = 2.5 +[repl.analysis] +passes = ['check_checker'] + +[repl.analysis.check_checker] +strict = false +trusted_sender = false +trusted_caller = false +callee_filter = false diff --git a/project-crowdfunding/README.md b/project-crowdfunding/README.md new file mode 100644 index 0000000..650b063 --- /dev/null +++ b/project-crowdfunding/README.md @@ -0,0 +1,211 @@ +# Open Source Project Funding Smart Contract + +## About +This Clarity smart contract enables decentralized funding for open-source projects on the Stacks blockchain. It provides a comprehensive system for creating, funding, and managing open-source projects with milestone-based development tracking and automated fund distribution. + +## Features + +### Project Management +- Create new open-source projects with detailed information +- Track project status and funding progress +- Milestone-based development tracking +- Automated fund distribution upon milestone completion + +### Funding Mechanism +- Secure STX token contributions +- Individual contributor tracking +- Prevention of over-funding +- Automatic milestone-based fund distribution + +### Access Control +- Project creator authorization +- Contract administrator controls +- Secure fund management +- Milestone completion verification + +## Contract Functions + +### Administrative Functions + +#### `initialize-contract` +Initializes the contract with the deployer as the administrator. +```clarity +(define-public (initialize-contract)) +``` + +### Project Management Functions + +#### `create-open-source-project` +Creates a new open-source project. +```clarity +(define-public (create-open-source-project + (project-title (string-ascii 100)) + (project-description (string-utf8 500)) + (target-funding-amount uint))) +``` +Parameters: +- `project-title`: Name of the project (max 100 characters) +- `project-description`: Detailed project description (max 500 characters) +- `target-funding-amount`: Total funding goal in microSTX + +#### `add-project-milestone` +Adds a development milestone to an existing project. +```clarity +(define-public (add-project-milestone + (project-identifier uint) + (milestone-title (string-ascii 100)) + (milestone-description (string-utf8 500)) + (milestone-deadline uint) + (milestone-funding-amount uint))) +``` +Parameters: +- `project-identifier`: Unique project ID +- `milestone-title`: Name of the milestone +- `milestone-description`: Detailed milestone description +- `milestone-deadline`: Block height deadline +- `milestone-funding-amount`: Funding amount for this milestone + +### Funding Functions + +#### `contribute-project-funding` +Contributes STX tokens to a project. +```clarity +(define-public (contribute-project-funding + (project-identifier uint) + (funding-amount uint))) +``` +Parameters: +- `project-identifier`: Project ID to fund +- `funding-amount`: Amount of STX tokens to contribute + +#### `complete-project-milestone` +Marks a milestone as complete and releases funds. +```clarity +(define-public (complete-project-milestone + (project-identifier uint) + (milestone-identifier uint))) +``` +Parameters: +- `project-identifier`: Project ID +- `milestone-identifier`: Milestone ID to complete + +### Read-Only Functions + +#### `get-project-details` +Retrieves project information. +```clarity +(define-read-only (get-project-details + (project-identifier uint))) +``` + +#### `get-milestone-details` +Retrieves milestone information. +```clarity +(define-read-only (get-milestone-details + (project-identifier uint) + (milestone-identifier uint))) +``` + +#### `get-contributor-funding-amount` +Gets the total contribution amount from a specific contributor. +```clarity +(define-read-only (get-contributor-funding-amount + (project-identifier uint) + (funding-contributor principal))) +``` + +#### `is-project-fully-funded` +Checks if a project has reached its funding goal. +```clarity +(define-read-only (is-project-fully-funded + (project-identifier uint))) +``` + +## Error Codes + +| Code | Description | +|------|-------------| +| ERR-UNAUTHORIZED-ACCESS | User not authorized for the operation | +| ERR-PROJECT-DOES-NOT-EXIST | Project ID not found | +| ERR-PROJECT-ALREADY-FUNDED | Project has already reached funding goal | +| ERR-INSUFFICIENT-BALANCE | Insufficient funds for operation | +| ERR-INVALID-FUNDING-AMOUNT | Invalid funding amount specified | +| ERR-MILESTONE-DOES-NOT-EXIST | Milestone ID not found | +| ERR-MILESTONE-INCOMPLETE | Milestone completion conditions not met | + +## Data Structures + +### Open Source Projects +```clarity +{ + project-creator: principal, + project-title: (string-ascii 100), + project-description: (string-utf8 500), + target-funding-amount: uint, + total-funds-raised: uint, + project-status: (string-ascii 20), + project-creation-block: uint +} +``` + +### Project Development Milestones +```clarity +{ + milestone-title: (string-ascii 100), + milestone-description: (string-utf8 500), + milestone-completion-deadline: uint, + milestone-funding-amount: uint, + milestone-status: (string-ascii 20) +} +``` + +### Project Funding Contributors +```clarity +{ + contribution-amount: uint +} +``` + +## Security Considerations + +1. **Access Control** + - Only project creators can add milestones and mark them as complete + - Only contract administrator can initialize the contract + - Public functions have appropriate authorization checks + +2. **Fund Safety** + - Automatic fund distribution upon milestone completion + - Prevention of over-funding + - Secure STX token transfers using contract principal + +3. **Input Validation** + - All numeric inputs are validated + - String lengths are constrained + - Milestone deadlines are verified + +## Usage Examples + +### Creating a New Project +```clarity +(contract-call? .open-source-funding create-open-source-project + "My Open Source Project" + "A detailed description of the project" + u1000000) +``` + +### Contributing Funds +```clarity +(contract-call? .open-source-funding contribute-project-funding + u1 + u500000) +``` + +### Adding a Milestone +```clarity +(contract-call? .open-source-funding add-project-milestone + u1 + "First Release" + "Complete core functionality" + u100000 + u500000) +``` \ No newline at end of file diff --git a/project-crowdfunding/Testnet.toml b/project-crowdfunding/Testnet.toml new file mode 100644 index 0000000..b9cfb45 --- /dev/null +++ b/project-crowdfunding/Testnet.toml @@ -0,0 +1,7 @@ +[network] +name = "testnet" +stacks_node_rpc_address = "https://api.testnet.hiro.so" +deployment_fee_rate = 10 + +[accounts.deployer] +mnemonic = "" diff --git a/project-crowdfunding/contracts/stx-project-funding.clar b/project-crowdfunding/contracts/stx-project-funding.clar new file mode 100644 index 0000000..d564f8f --- /dev/null +++ b/project-crowdfunding/contracts/stx-project-funding.clar @@ -0,0 +1,202 @@ +;; Open Source Project Funding Contract +;; This contract allows users to create and fund open source projects + +;; Error codes +(define-constant ERR-UNAUTHORIZED-ACCESS (err u1)) +(define-constant ERR-PROJECT-DOES-NOT-EXIST (err u2)) +(define-constant ERR-PROJECT-ALREADY-FUNDED (err u3)) +(define-constant ERR-INSUFFICIENT-BALANCE (err u4)) +(define-constant ERR-INVALID-FUNDING-AMOUNT (err u5)) +(define-constant ERR-MILESTONE-DOES-NOT-EXIST (err u6)) +(define-constant ERR-MILESTONE-INCOMPLETE (err u7)) + +;; Data variables +(define-data-var contract-administrator principal tx-sender) +(define-map open-source-projects + { project-identifier: uint } + { + project-creator: principal, + project-title: (string-ascii 100), + project-description: (string-utf8 500), + target-funding-amount: uint, + total-funds-raised: uint, + project-status: (string-ascii 20), + project-creation-block: uint + } +) + +(define-map project-development-milestones + { project-identifier: uint, milestone-identifier: uint } + { + milestone-title: (string-ascii 100), + milestone-description: (string-utf8 500), + milestone-completion-deadline: uint, + milestone-funding-amount: uint, + milestone-status: (string-ascii 20) + } +) + +(define-map project-funding-contributors + { project-identifier: uint, funding-contributor: principal } + { contribution-amount: uint } +) + +;; Counter for project IDs +(define-data-var next-project-identifier uint u0) + +;; Initialize contract +(define-public (initialize-contract) + (begin + (asserts! (is-eq tx-sender (var-get contract-administrator)) ERR-UNAUTHORIZED-ACCESS) + (ok true) + ) +) + +;; Create a new project +(define-public (create-open-source-project + (project-title (string-ascii 100)) + (project-description (string-utf8 500)) + (target-funding-amount uint)) + (let ((new-project-identifier (+ (var-get next-project-identifier) u1))) + (asserts! (> target-funding-amount u0) ERR-INVALID-FUNDING-AMOUNT) + (try! (map-insert open-source-projects + { project-identifier: new-project-identifier } + { + project-creator: tx-sender, + project-title: project-title, + project-description: project-description, + target-funding-amount: target-funding-amount, + total-funds-raised: u0, + project-status: "active", + project-creation-block: block-height + } + )) + (var-set next-project-identifier new-project-identifier) + (ok new-project-identifier) + ) +) + +;; Add milestone to project +(define-public (add-project-milestone + (project-identifier uint) + (milestone-title (string-ascii 100)) + (milestone-description (string-utf8 500)) + (milestone-deadline uint) + (milestone-funding-amount uint)) + (let ((project-details (unwrap! (map-get? open-source-projects { project-identifier: project-identifier }) ERR-PROJECT-DOES-NOT-EXIST))) + (asserts! (is-eq (get project-creator project-details) tx-sender) ERR-UNAUTHORIZED-ACCESS) + (asserts! (>= (get target-funding-amount project-details) milestone-funding-amount) ERR-INVALID-FUNDING-AMOUNT) + (try! (map-insert project-development-milestones + { + project-identifier: project-identifier, + milestone-identifier: u1 + } + { + milestone-title: milestone-title, + milestone-description: milestone-description, + milestone-completion-deadline: milestone-deadline, + milestone-funding-amount: milestone-funding-amount, + milestone-status: "pending" + } + )) + (ok true) + ) +) + +;; Fund a project +(define-public (contribute-project-funding (project-identifier uint) (funding-amount uint)) + (let ( + (project-details (unwrap! (map-get? open-source-projects { project-identifier: project-identifier }) ERR-PROJECT-DOES-NOT-EXIST)) + (current-project-funding (get total-funds-raised project-details)) + (project-funding-goal (get target-funding-amount project-details)) + ) + (asserts! (is-eq (get project-status project-details) "active") ERR-PROJECT-ALREADY-FUNDED) + (asserts! (> funding-amount u0) ERR-INVALID-FUNDING-AMOUNT) + (asserts! (<= (+ current-project-funding funding-amount) project-funding-goal) ERR-INVALID-FUNDING-AMOUNT) + + ;; Transfer STX from sender to contract + (try! (stx-transfer? funding-amount tx-sender (as-contract tx-sender))) + + ;; Update project funding + (map-set open-source-projects + { project-identifier: project-identifier } + (merge project-details { + total-funds-raised: (+ current-project-funding funding-amount) + }) + ) + + ;; Record contributor funding + (match (map-get? project-funding-contributors + { project-identifier: project-identifier, funding-contributor: tx-sender }) + previous-contribution + (map-set project-funding-contributors + { project-identifier: project-identifier, funding-contributor: tx-sender } + { contribution-amount: (+ funding-amount + (default-to u0 (get contribution-amount previous-contribution))) } + ) + (map-insert project-funding-contributors + { project-identifier: project-identifier, funding-contributor: tx-sender } + { contribution-amount: funding-amount } + ) + ) + + (ok true) + ) +) + +;; Complete milestone +(define-public (complete-project-milestone (project-identifier uint) (milestone-identifier uint)) + (let ( + (project-details (unwrap! (map-get? open-source-projects + { project-identifier: project-identifier }) ERR-PROJECT-DOES-NOT-EXIST)) + (milestone-details (unwrap! (map-get? project-development-milestones + { project-identifier: project-identifier, milestone-identifier: milestone-identifier }) + ERR-MILESTONE-DOES-NOT-EXIST)) + ) + (asserts! (is-eq (get project-creator project-details) tx-sender) ERR-UNAUTHORIZED-ACCESS) + (asserts! (>= block-height (get milestone-completion-deadline milestone-details)) ERR-MILESTONE-INCOMPLETE) + + ;; Update milestone status + (map-set project-development-milestones + { project-identifier: project-identifier, milestone-identifier: milestone-identifier } + (merge milestone-details { milestone-status: "completed" }) + ) + + ;; Transfer milestone amount to project creator + (try! (as-contract (stx-transfer? + (get milestone-funding-amount milestone-details) + tx-sender + (get project-creator project-details)))) + + (ok true) + ) +) + +;; Get project details +(define-read-only (get-project-details (project-identifier uint)) + (ok (unwrap! (map-get? open-source-projects + { project-identifier: project-identifier }) ERR-PROJECT-DOES-NOT-EXIST)) +) + +;; Get project milestone details +(define-read-only (get-milestone-details (project-identifier uint) (milestone-identifier uint)) + (ok (unwrap! (map-get? project-development-milestones + { project-identifier: project-identifier, milestone-identifier: milestone-identifier }) + ERR-MILESTONE-DOES-NOT-EXIST)) +) + +;; Get contributor funding amount +(define-read-only (get-contributor-funding-amount (project-identifier uint) (funding-contributor principal)) + (ok (unwrap! (map-get? project-funding-contributors + { project-identifier: project-identifier, funding-contributor: funding-contributor }) + ERR-PROJECT-DOES-NOT-EXIST)) +) + +;; Check if project is fully funded +(define-read-only (is-project-fully-funded (project-identifier uint)) + (match (map-get? open-source-projects { project-identifier: project-identifier }) + project-details (ok (>= (get total-funds-raised project-details) + (get target-funding-amount project-details))) + ERR-PROJECT-DOES-NOT-EXIST + ) +) \ No newline at end of file diff --git a/project-crowdfunding/package.json b/project-crowdfunding/package.json new file mode 100644 index 0000000..50f294d --- /dev/null +++ b/project-crowdfunding/package.json @@ -0,0 +1,24 @@ + +{ + "name": "project-crowdfunding-tests", + "version": "1.0.0", + "description": "Run unit tests on this project.", + "type": "module", + "private": true, + "scripts": { + "test": "vitest run", + "test:report": "vitest run -- --coverage --costs", + "test:watch": "chokidar \"tests/**/*.ts\" \"contracts/**/*.clar\" -c \"npm run test:report\"" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@hirosystems/clarinet-sdk": "^2.3.2", + "@stacks/transactions": "^6.12.0", + "chokidar-cli": "^3.0.0", + "typescript": "^5.3.3", + "vite": "^5.1.4", + "vitest": "^1.3.1", + "vitest-environment-clarinet": "^2.0.0" + } +} diff --git a/project-crowdfunding/settings/Devnet.toml b/project-crowdfunding/settings/Devnet.toml new file mode 100644 index 0000000..7d865b9 --- /dev/null +++ b/project-crowdfunding/settings/Devnet.toml @@ -0,0 +1,151 @@ +[network] +name = "devnet" +deployment_fee_rate = 10 + +[accounts.deployer] +mnemonic = "twice kind fence tip hidden tilt action fragile skin nothing glory cousin green tomorrow spring wrist shed math olympic multiply hip blue scout claw" +balance = 100_000_000_000_000 +# secret_key: 753b7cc01a1a2e86221266a154af739463fce51219d97e4f856cd7200c3bd2a601 +# stx_address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM +# btc_address: mqVnk6NPRdhntvfm4hh9vvjiRkFDUuSYsH + +[accounts.wallet_1] +mnemonic = "sell invite acquire kitten bamboo drastic jelly vivid peace spawn twice guilt pave pen trash pretty park cube fragile unaware remain midnight betray rebuild" +balance = 100_000_000_000_000 +# secret_key: 7287ba251d44a4d3fd9276c88ce34c5c52a038955511cccaf77e61068649c17801 +# stx_address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 +# btc_address: mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC + +[accounts.wallet_2] +mnemonic = "hold excess usual excess ring elephant install account glad dry fragile donkey gaze humble truck breeze nation gasp vacuum limb head keep delay hospital" +balance = 100_000_000_000_000 +# secret_key: 530d9f61984c888536871c6573073bdfc0058896dc1adfe9a6a10dfacadc209101 +# stx_address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG +# btc_address: muYdXKmX9bByAueDe6KFfHd5Ff1gdN9ErG + +[accounts.wallet_3] +mnemonic = "cycle puppy glare enroll cost improve round trend wrist mushroom scorpion tower claim oppose clever elephant dinosaur eight problem before frozen dune wagon high" +balance = 100_000_000_000_000 +# secret_key: d655b2523bcd65e34889725c73064feb17ceb796831c0e111ba1a552b0f31b3901 +# stx_address: ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC +# btc_address: mvZtbibDAAA3WLpY7zXXFqRa3T4XSknBX7 + +[accounts.wallet_4] +mnemonic = "board list obtain sugar hour worth raven scout denial thunder horse logic fury scorpion fold genuine phrase wealth news aim below celery when cabin" +balance = 100_000_000_000_000 +# secret_key: f9d7206a47f14d2870c163ebab4bf3e70d18f5d14ce1031f3902fbbc894fe4c701 +# stx_address: ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND +# btc_address: mg1C76bNTutiCDV3t9nWhZs3Dc8LzUufj8 + +[accounts.wallet_5] +mnemonic = "hurry aunt blame peanut heavy update captain human rice crime juice adult scale device promote vast project quiz unit note reform update climb purchase" +balance = 100_000_000_000_000 +# secret_key: 3eccc5dac8056590432db6a35d52b9896876a3d5cbdea53b72400bc9c2099fe801 +# stx_address: ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB +# btc_address: mweN5WVqadScHdA81aATSdcVr4B6dNokqx + +[accounts.wallet_6] +mnemonic = "area desk dutch sign gold cricket dawn toward giggle vibrant indoor bench warfare wagon number tiny universe sand talk dilemma pottery bone trap buddy" +balance = 100_000_000_000_000 +# secret_key: 7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 +# stx_address: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0 +# btc_address: mzxXgV6e4BZSsz8zVHm3TmqbECt7mbuErt + +[accounts.wallet_7] +mnemonic = "prevent gallery kind limb income control noise together echo rival record wedding sense uncover school version force bleak nuclear include danger skirt enact arrow" +balance = 100_000_000_000_000 +# secret_key: b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401 +# stx_address: ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ +# btc_address: n37mwmru2oaVosgfuvzBwgV2ysCQRrLko7 + +[accounts.wallet_8] +mnemonic = "female adjust gallery certain visit token during great side clown fitness like hurt clip knife warm bench start reunion globe detail dream depend fortune" +balance = 100_000_000_000_000 +# secret_key: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01 +# stx_address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP +# btc_address: n2v875jbJ4RjBnTjgbfikDfnwsDV5iUByw + +[accounts.faucet] +mnemonic = "shadow private easily thought say logic fault paddle word top book during ignore notable orange flight clock image wealth health outside kitten belt reform" +balance = 100_000_000_000_000 +# secret_key: de433bdfa14ec43aa1098d5be594c8ffb20a31485ff9de2923b2689471c401b801 +# stx_address: STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6 +# btc_address: mjSrB3wS4xab3kYqFktwBzfTdPg367ZJ2d + +[devnet] +disable_stacks_explorer = false +disable_stacks_api = false +# disable_subnet_api = false +# disable_bitcoin_explorer = true +# working_dir = "tmp/devnet" +# stacks_node_events_observers = ["host.docker.internal:8002"] +# miner_mnemonic = "fragile loan twenty basic net assault jazz absorb diet talk art shock innocent float punch travel gadget embrace caught blossom hockey surround initial reduce" +# miner_derivation_path = "m/44'/5757'/0'/0/0" +# faucet_mnemonic = "shadow private easily thought say logic fault paddle word top book during ignore notable orange flight clock image wealth health outside kitten belt reform" +# faucet_derivation_path = "m/44'/5757'/0'/0/0" +# orchestrator_port = 20445 +# bitcoin_node_p2p_port = 18444 +# bitcoin_node_rpc_port = 18443 +# bitcoin_node_username = "devnet" +# bitcoin_node_password = "devnet" +# bitcoin_controller_block_time = 30_000 +# stacks_node_rpc_port = 20443 +# stacks_node_p2p_port = 20444 +# stacks_api_port = 3999 +# stacks_api_events_port = 3700 +# bitcoin_explorer_port = 8001 +# stacks_explorer_port = 8000 +# postgres_port = 5432 +# postgres_username = "postgres" +# postgres_password = "postgres" +# postgres_database = "postgres" +# bitcoin_node_image_url = "quay.io/hirosystems/bitcoind:26.0" +# stacks_node_image_url = "quay.io/hirosystems/stacks-node:devnet-2.5" +# stacks_signer_image_url = "quay.io/hirosystems/stacks-signer:devnet-2.5" +# stacks_api_image_url = "hirosystems/stacks-blockchain-api:master" +# stacks_explorer_image_url = "hirosystems/explorer:latest" +# bitcoin_explorer_image_url = "quay.io/hirosystems/bitcoin-explorer:devnet" +# postgres_image_url = "postgres:alpine" +# enable_subnet_node = true +# subnet_node_image_url = "hirosystems/stacks-subnets:0.8.1" +# subnet_leader_mnemonic = "twice kind fence tip hidden tilt action fragile skin nothing glory cousin green tomorrow spring wrist shed math olympic multiply hip blue scout claw" +# subnet_leader_derivation_path = "m/44'/5757'/0'/0/0" +# subnet_contract_id = "ST173JK7NZBA4BS05ZRATQH1K89YJMTGEH1Z5J52E.subnet-v3-0-1" +# subnet_node_rpc_port = 30443 +# subnet_node_p2p_port = 30444 +# subnet_events_ingestion_port = 30445 +# subnet_node_events_observers = ["host.docker.internal:8002"] +# subnet_api_image_url = "hirosystems/stacks-blockchain-api:master" +# subnet_api_postgres_database = "subnet_api" + +# For testing in epoch 2.1 / using Clarity2 +# epoch_2_0 = 100 +# epoch_2_05 = 100 +# epoch_2_1 = 101 +# epoch_2_2 = 102 +# epoch_2_3 = 103 +# epoch_2_4 = 104 +# epoch_2_5 = 108 + + +# Send some stacking orders +[[devnet.pox_stacking_orders]] +start_at_cycle = 1 +duration = 12 +wallet = "wallet_1" +slots = 2 +btc_address = "mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC" + +[[devnet.pox_stacking_orders]] +start_at_cycle = 1 +duration = 12 +wallet = "wallet_2" +slots = 1 +btc_address = "muYdXKmX9bByAueDe6KFfHd5Ff1gdN9ErG" + +[[devnet.pox_stacking_orders]] +start_at_cycle = 1 +duration = 12 +wallet = "wallet_3" +slots = 1 +btc_address = "mvZtbibDAAA3WLpY7zXXFqRa3T4XSknBX7" diff --git a/project-crowdfunding/tests/stx-project-funding.test.ts b/project-crowdfunding/tests/stx-project-funding.test.ts new file mode 100644 index 0000000..f2d4bb8 --- /dev/null +++ b/project-crowdfunding/tests/stx-project-funding.test.ts @@ -0,0 +1,21 @@ + +import { describe, expect, it } from "vitest"; + +const accounts = simnet.getAccounts(); +const address1 = accounts.get("wallet_1")!; + +/* + The test below is an example. To learn more, read the testing documentation here: + https://docs.hiro.so/clarinet/feature-guides/test-contract-with-clarinet-sdk +*/ + +describe("example tests", () => { + it("ensures simnet is well initalised", () => { + expect(simnet.blockHeight).toBeDefined(); + }); + + // it("shows an example", () => { + // const { result } = simnet.callReadOnlyFn("counter", "get-counter", [], address1); + // expect(result).toBeUint(0); + // }); +}); diff --git a/project-crowdfunding/tsconfig.json b/project-crowdfunding/tsconfig.json new file mode 100644 index 0000000..1bdaf36 --- /dev/null +++ b/project-crowdfunding/tsconfig.json @@ -0,0 +1,26 @@ + +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext"], + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + "strict": true, + "noImplicitAny": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "node_modules/@hirosystems/clarinet-sdk/vitest-helpers/src", + "tests" + ] +} diff --git a/project-crowdfunding/vitest.config.js b/project-crowdfunding/vitest.config.js new file mode 100644 index 0000000..c6a8506 --- /dev/null +++ b/project-crowdfunding/vitest.config.js @@ -0,0 +1,42 @@ + +/// + +import { defineConfig } from "vite"; +import { vitestSetupFilePath, getClarinetVitestsArgv } from "@hirosystems/clarinet-sdk/vitest"; + +/* + In this file, Vitest is configured so that it works seamlessly with Clarinet and the Simnet. + + The `vitest-environment-clarinet` will initialise the clarinet-sdk + and make the `simnet` object available globally in the test files. + + `vitestSetupFilePath` points to a file in the `@hirosystems/clarinet-sdk` package that does two things: + - run `before` hooks to initialize the simnet and `after` hooks to collect costs and coverage reports. + - load custom vitest matchers to work with Clarity values (such as `expect(...).toBeUint()`) + + The `getClarinetVitestsArgv()` will parse options passed to the command `vitest run --` + - vitest run -- --manifest ./Clarinet.toml # pass a custom path + - vitest run -- --coverage --costs # collect coverage and cost reports +*/ + +export default defineConfig({ + test: { + environment: "clarinet", // use vitest-environment-clarinet + pool: "forks", + poolOptions: { + threads: { singleThread: true }, + forks: { singleFork: true }, + }, + setupFiles: [ + vitestSetupFilePath, + // custom setup files can be added here + ], + environmentOptions: { + clarinet: { + ...getClarinetVitestsArgv(), + // add or override options + }, + }, + }, +}); + From ac39d99f648e35222e8824586adb323f801cf061 Mon Sep 17 00:00:00 2001 From: Vicky08100 Date: Thu, 12 Dec 2024 16:33:43 +0100 Subject: [PATCH 2/3] Fixed all errors in the smart contract --- .../contracts/stx-project-funding.clar | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/project-crowdfunding/contracts/stx-project-funding.clar b/project-crowdfunding/contracts/stx-project-funding.clar index d564f8f..1748f4d 100644 --- a/project-crowdfunding/contracts/stx-project-funding.clar +++ b/project-crowdfunding/contracts/stx-project-funding.clar @@ -59,7 +59,7 @@ (target-funding-amount uint)) (let ((new-project-identifier (+ (var-get next-project-identifier) u1))) (asserts! (> target-funding-amount u0) ERR-INVALID-FUNDING-AMOUNT) - (try! (map-insert open-source-projects + (map-insert open-source-projects { project-identifier: new-project-identifier } { project-creator: tx-sender, @@ -70,7 +70,7 @@ project-status: "active", project-creation-block: block-height } - )) + ) (var-set next-project-identifier new-project-identifier) (ok new-project-identifier) ) @@ -86,7 +86,7 @@ (let ((project-details (unwrap! (map-get? open-source-projects { project-identifier: project-identifier }) ERR-PROJECT-DOES-NOT-EXIST))) (asserts! (is-eq (get project-creator project-details) tx-sender) ERR-UNAUTHORIZED-ACCESS) (asserts! (>= (get target-funding-amount project-details) milestone-funding-amount) ERR-INVALID-FUNDING-AMOUNT) - (try! (map-insert project-development-milestones + (map-insert project-development-milestones { project-identifier: project-identifier, milestone-identifier: u1 @@ -98,7 +98,7 @@ milestone-funding-amount: milestone-funding-amount, milestone-status: "pending" } - )) + ) (ok true) ) ) @@ -131,8 +131,7 @@ previous-contribution (map-set project-funding-contributors { project-identifier: project-identifier, funding-contributor: tx-sender } - { contribution-amount: (+ funding-amount - (default-to u0 (get contribution-amount previous-contribution))) } + { contribution-amount: (+ funding-amount (get contribution-amount previous-contribution)) } ) (map-insert project-funding-contributors { project-identifier: project-identifier, funding-contributor: tx-sender } From 170ce8ece0bb9900a39d552dacd3593ab5abba01 Mon Sep 17 00:00:00 2001 From: Vicky08100 Date: Thu, 12 Dec 2024 16:46:20 +0100 Subject: [PATCH 3/3] Fixed all warnings and security issues in the smart contract by adding validations --- .../contracts/stx-project-funding.clar | 275 ++++++++++++------ 1 file changed, 181 insertions(+), 94 deletions(-) diff --git a/project-crowdfunding/contracts/stx-project-funding.clar b/project-crowdfunding/contracts/stx-project-funding.clar index 1748f4d..ed31411 100644 --- a/project-crowdfunding/contracts/stx-project-funding.clar +++ b/project-crowdfunding/contracts/stx-project-funding.clar @@ -9,6 +9,13 @@ (define-constant ERR-INVALID-FUNDING-AMOUNT (err u5)) (define-constant ERR-MILESTONE-DOES-NOT-EXIST (err u6)) (define-constant ERR-MILESTONE-INCOMPLETE (err u7)) +(define-constant ERR-INVALID-PROJECT-TITLE (err u8)) +(define-constant ERR-INVALID-PROJECT-DESCRIPTION (err u9)) +(define-constant ERR-INVALID-MILESTONE-TITLE (err u10)) +(define-constant ERR-INVALID-MILESTONE-DESCRIPTION (err u11)) +(define-constant ERR-INVALID-DEADLINE (err u12)) +(define-constant ERR-INVALID-PROJECT-ID (err u13)) +(define-constant ERR-INVALID-MILESTONE-ID (err u14)) ;; Data variables (define-data-var contract-administrator principal tx-sender) @@ -44,6 +51,30 @@ ;; Counter for project IDs (define-data-var next-project-identifier uint u0) +;; Helper functions for validation +(define-private (is-valid-string-ascii (value (string-ascii 100))) + (> (len value) u0) +) + +(define-private (is-valid-string-utf8 (value (string-utf8 500))) + (> (len value) u0) +) + +(define-private (is-valid-project-id (project-id uint)) + (and + (> project-id u0) + (<= project-id (var-get next-project-identifier)) + ) +) + +(define-private (is-valid-milestone-id (milestone-id uint)) + (> milestone-id u0) +) + +(define-private (is-valid-deadline (deadline uint)) + (>= deadline block-height) +) + ;; Initialize contract (define-public (initialize-contract) (begin @@ -57,22 +88,28 @@ (project-title (string-ascii 100)) (project-description (string-utf8 500)) (target-funding-amount uint)) - (let ((new-project-identifier (+ (var-get next-project-identifier) u1))) + (begin + ;; Validate inputs + (asserts! (is-valid-string-ascii project-title) ERR-INVALID-PROJECT-TITLE) + (asserts! (is-valid-string-utf8 project-description) ERR-INVALID-PROJECT-DESCRIPTION) (asserts! (> target-funding-amount u0) ERR-INVALID-FUNDING-AMOUNT) - (map-insert open-source-projects - { project-identifier: new-project-identifier } - { - project-creator: tx-sender, - project-title: project-title, - project-description: project-description, - target-funding-amount: target-funding-amount, - total-funds-raised: u0, - project-status: "active", - project-creation-block: block-height - } + + (let ((new-project-identifier (+ (var-get next-project-identifier) u1))) + (map-insert open-source-projects + { project-identifier: new-project-identifier } + { + project-creator: tx-sender, + project-title: project-title, + project-description: project-description, + target-funding-amount: target-funding-amount, + total-funds-raised: u0, + project-status: "active", + project-creation-block: block-height + } + ) + (var-set next-project-identifier new-project-identifier) + (ok new-project-identifier) ) - (var-set next-project-identifier new-project-identifier) - (ok new-project-identifier) ) ) @@ -83,119 +120,169 @@ (milestone-description (string-utf8 500)) (milestone-deadline uint) (milestone-funding-amount uint)) - (let ((project-details (unwrap! (map-get? open-source-projects { project-identifier: project-identifier }) ERR-PROJECT-DOES-NOT-EXIST))) - (asserts! (is-eq (get project-creator project-details) tx-sender) ERR-UNAUTHORIZED-ACCESS) - (asserts! (>= (get target-funding-amount project-details) milestone-funding-amount) ERR-INVALID-FUNDING-AMOUNT) - (map-insert project-development-milestones - { - project-identifier: project-identifier, - milestone-identifier: u1 - } - { - milestone-title: milestone-title, - milestone-description: milestone-description, - milestone-completion-deadline: milestone-deadline, - milestone-funding-amount: milestone-funding-amount, - milestone-status: "pending" - } + (begin + ;; Validate inputs + (asserts! (is-valid-project-id project-identifier) ERR-INVALID-PROJECT-ID) + (asserts! (is-valid-string-ascii milestone-title) ERR-INVALID-MILESTONE-TITLE) + (asserts! (is-valid-string-utf8 milestone-description) ERR-INVALID-MILESTONE-DESCRIPTION) + (asserts! (is-valid-deadline milestone-deadline) ERR-INVALID-DEADLINE) + + (let ((project-details (unwrap! (map-get? open-source-projects { project-identifier: project-identifier }) ERR-PROJECT-DOES-NOT-EXIST))) + (asserts! (is-eq (get project-creator project-details) tx-sender) ERR-UNAUTHORIZED-ACCESS) + (asserts! (>= (get target-funding-amount project-details) milestone-funding-amount) ERR-INVALID-FUNDING-AMOUNT) + + (map-insert project-development-milestones + { + project-identifier: project-identifier, + milestone-identifier: u1 + } + { + milestone-title: milestone-title, + milestone-description: milestone-description, + milestone-completion-deadline: milestone-deadline, + milestone-funding-amount: milestone-funding-amount, + milestone-status: "pending" + } + ) + (ok true) ) - (ok true) ) ) ;; Fund a project (define-public (contribute-project-funding (project-identifier uint) (funding-amount uint)) - (let ( - (project-details (unwrap! (map-get? open-source-projects { project-identifier: project-identifier }) ERR-PROJECT-DOES-NOT-EXIST)) - (current-project-funding (get total-funds-raised project-details)) - (project-funding-goal (get target-funding-amount project-details)) - ) - (asserts! (is-eq (get project-status project-details) "active") ERR-PROJECT-ALREADY-FUNDED) - (asserts! (> funding-amount u0) ERR-INVALID-FUNDING-AMOUNT) - (asserts! (<= (+ current-project-funding funding-amount) project-funding-goal) ERR-INVALID-FUNDING-AMOUNT) - - ;; Transfer STX from sender to contract - (try! (stx-transfer? funding-amount tx-sender (as-contract tx-sender))) + (begin + (asserts! (is-valid-project-id project-identifier) ERR-INVALID-PROJECT-ID) - ;; Update project funding - (map-set open-source-projects - { project-identifier: project-identifier } - (merge project-details { - total-funds-raised: (+ current-project-funding funding-amount) - }) + (let ( + (project-details (unwrap! (map-get? open-source-projects { project-identifier: project-identifier }) ERR-PROJECT-DOES-NOT-EXIST)) + (current-project-funding (get total-funds-raised project-details)) + (project-funding-goal (get target-funding-amount project-details)) ) - - ;; Record contributor funding - (match (map-get? project-funding-contributors - { project-identifier: project-identifier, funding-contributor: tx-sender }) - previous-contribution - (map-set project-funding-contributors - { project-identifier: project-identifier, funding-contributor: tx-sender } - { contribution-amount: (+ funding-amount (get contribution-amount previous-contribution)) } + (asserts! (is-eq (get project-status project-details) "active") ERR-PROJECT-ALREADY-FUNDED) + (asserts! (> funding-amount u0) ERR-INVALID-FUNDING-AMOUNT) + (asserts! (<= (+ current-project-funding funding-amount) project-funding-goal) ERR-INVALID-FUNDING-AMOUNT) + + ;; Transfer STX from sender to contract + (try! (stx-transfer? funding-amount tx-sender (as-contract tx-sender))) + + ;; Update project funding + (map-set open-source-projects + { project-identifier: project-identifier } + (merge project-details { + total-funds-raised: (+ current-project-funding funding-amount) + }) ) - (map-insert project-funding-contributors - { project-identifier: project-identifier, funding-contributor: tx-sender } - { contribution-amount: funding-amount } + + ;; Record contributor funding + (match (map-get? project-funding-contributors + { project-identifier: project-identifier, funding-contributor: tx-sender }) + previous-contribution + (map-set project-funding-contributors + { project-identifier: project-identifier, funding-contributor: tx-sender } + { contribution-amount: (+ funding-amount (get contribution-amount previous-contribution)) } + ) + (map-insert project-funding-contributors + { project-identifier: project-identifier, funding-contributor: tx-sender } + { contribution-amount: funding-amount } + ) ) + + (ok true) ) - - (ok true) ) ) ;; Complete milestone (define-public (complete-project-milestone (project-identifier uint) (milestone-identifier uint)) - (let ( - (project-details (unwrap! (map-get? open-source-projects - { project-identifier: project-identifier }) ERR-PROJECT-DOES-NOT-EXIST)) - (milestone-details (unwrap! (map-get? project-development-milestones - { project-identifier: project-identifier, milestone-identifier: milestone-identifier }) - ERR-MILESTONE-DOES-NOT-EXIST)) - ) - (asserts! (is-eq (get project-creator project-details) tx-sender) ERR-UNAUTHORIZED-ACCESS) - (asserts! (>= block-height (get milestone-completion-deadline milestone-details)) ERR-MILESTONE-INCOMPLETE) + (begin + (asserts! (is-valid-project-id project-identifier) ERR-INVALID-PROJECT-ID) + (asserts! (is-valid-milestone-id milestone-identifier) ERR-INVALID-MILESTONE-ID) - ;; Update milestone status - (map-set project-development-milestones - { project-identifier: project-identifier, milestone-identifier: milestone-identifier } - (merge milestone-details { milestone-status: "completed" }) + (let ( + (project-details (unwrap! (map-get? open-source-projects + { project-identifier: project-identifier }) ERR-PROJECT-DOES-NOT-EXIST)) + (milestone-details (unwrap! (map-get? project-development-milestones + { project-identifier: project-identifier, milestone-identifier: milestone-identifier }) + ERR-MILESTONE-DOES-NOT-EXIST)) + ) + (asserts! (is-eq (get project-creator project-details) tx-sender) ERR-UNAUTHORIZED-ACCESS) + (asserts! (>= block-height (get milestone-completion-deadline milestone-details)) ERR-MILESTONE-INCOMPLETE) + + ;; Update milestone status + (map-set project-development-milestones + { project-identifier: project-identifier, milestone-identifier: milestone-identifier } + (merge milestone-details { milestone-status: "completed" }) + ) + + ;; Transfer milestone amount to project creator + (try! (as-contract (stx-transfer? + (get milestone-funding-amount milestone-details) + tx-sender + (get project-creator project-details)))) + + (ok true) ) - - ;; Transfer milestone amount to project creator - (try! (as-contract (stx-transfer? - (get milestone-funding-amount milestone-details) - tx-sender - (get project-creator project-details)))) - - (ok true) ) ) +;; Read-only functions ;; Get project details (define-read-only (get-project-details (project-identifier uint)) - (ok (unwrap! (map-get? open-source-projects - { project-identifier: project-identifier }) ERR-PROJECT-DOES-NOT-EXIST)) + (begin + (asserts! (is-valid-project-id project-identifier) ERR-INVALID-PROJECT-ID) + (ok (unwrap! (map-get? open-source-projects + { project-identifier: project-identifier }) ERR-PROJECT-DOES-NOT-EXIST)) + ) ) -;; Get project milestone details +;; Get milestone details (define-read-only (get-milestone-details (project-identifier uint) (milestone-identifier uint)) - (ok (unwrap! (map-get? project-development-milestones - { project-identifier: project-identifier, milestone-identifier: milestone-identifier }) - ERR-MILESTONE-DOES-NOT-EXIST)) + (begin + (asserts! (is-valid-project-id project-identifier) ERR-INVALID-PROJECT-ID) + (asserts! (is-valid-milestone-id milestone-identifier) ERR-INVALID-MILESTONE-ID) + (ok (unwrap! (map-get? project-development-milestones + { project-identifier: project-identifier, milestone-identifier: milestone-identifier }) + ERR-MILESTONE-DOES-NOT-EXIST)) + ) +) + +;; Get total number of projects +(define-read-only (get-total-projects) + (ok (var-get next-project-identifier)) ) ;; Get contributor funding amount (define-read-only (get-contributor-funding-amount (project-identifier uint) (funding-contributor principal)) - (ok (unwrap! (map-get? project-funding-contributors - { project-identifier: project-identifier, funding-contributor: funding-contributor }) - ERR-PROJECT-DOES-NOT-EXIST)) + (begin + (asserts! (is-valid-project-id project-identifier) ERR-INVALID-PROJECT-ID) + (ok (unwrap! (map-get? project-funding-contributors + { project-identifier: project-identifier, funding-contributor: funding-contributor }) + ERR-PROJECT-DOES-NOT-EXIST)) + ) ) ;; Check if project is fully funded (define-read-only (is-project-fully-funded (project-identifier uint)) - (match (map-get? open-source-projects { project-identifier: project-identifier }) - project-details (ok (>= (get total-funds-raised project-details) - (get target-funding-amount project-details))) - ERR-PROJECT-DOES-NOT-EXIST + (begin + (asserts! (is-valid-project-id project-identifier) ERR-INVALID-PROJECT-ID) + (match (map-get? open-source-projects { project-identifier: project-identifier }) + project-details (ok (>= (get total-funds-raised project-details) + (get target-funding-amount project-details))) + ERR-PROJECT-DOES-NOT-EXIST + ) + ) +) + +;; Check if milestone is completed +(define-read-only (is-milestone-completed (project-identifier uint) (milestone-identifier uint)) + (begin + (asserts! (is-valid-project-id project-identifier) ERR-INVALID-PROJECT-ID) + (asserts! (is-valid-milestone-id milestone-identifier) ERR-INVALID-MILESTONE-ID) + (match (map-get? project-development-milestones + { project-identifier: project-identifier, milestone-identifier: milestone-identifier }) + milestone-details (ok (is-eq (get milestone-status milestone-details) "completed")) + ERR-MILESTONE-DOES-NOT-EXIST + ) ) ) \ No newline at end of file