diff --git a/Research/.gitignore b/Research/.gitignore new file mode 100644 index 0000000..a56ad9b --- /dev/null +++ b/Research/.gitignore @@ -0,0 +1,5 @@ + +**/settings/Mainnet.toml +**/settings/Testnet.toml +.requirements/ +history.txt diff --git a/Research/.vscode/settings.json b/Research/.vscode/settings.json new file mode 100644 index 0000000..94da4be --- /dev/null +++ b/Research/.vscode/settings.json @@ -0,0 +1,5 @@ + +{ + "deno.enable": true, + "files.eol": "\n" +} diff --git a/Research/.vscode/tasks.json b/Research/.vscode/tasks.json new file mode 100644 index 0000000..22af91c --- /dev/null +++ b/Research/.vscode/tasks.json @@ -0,0 +1,18 @@ + +{ + "version": "2.0.0", + "tasks": [ + { + "label": "check contracts", + "group": "test", + "type": "shell", + "command": "clarinet check" + }, + { + "label": "test contracts", + "group": "test", + "type": "shell", + "command": "clarinet test" + } + ] +} diff --git a/Research/Clarinet.toml b/Research/Clarinet.toml new file mode 100644 index 0000000..a437a2e --- /dev/null +++ b/Research/Clarinet.toml @@ -0,0 +1,23 @@ +[project] +name = "Research" +authors = [] +description = "" +telemetry = true +requirements = [] +cache_dir = "/home/runner/Clar/Hillz/Research/./.requirements" +boot_contracts = ["pox", "costs-v2", "bns"] +[contracts.academic-research] +path = "contracts/academic-research.clar" + +[repl] +costs_version = 2 +parser_version = 2 + +[repl.analysis] +passes = ["check_checker"] + +[repl.analysis.check_checker] +strict = false +trusted_sender = false +trusted_caller = false +callee_filter = false diff --git a/Research/contracts/academic-research.clar b/Research/contracts/academic-research.clar new file mode 100644 index 0000000..6cbbff6 --- /dev/null +++ b/Research/contracts/academic-research.clar @@ -0,0 +1,305 @@ +;; Academic Research Funding Smart Contract + +;; Define constants +(define-constant CONTRACT_OWNER tx-sender) +(define-constant ERR_NOT_AUTHORIZED (err u100)) +(define-constant ERR_INVALID_AMOUNT (err u101)) +(define-constant ERR_PROPOSAL_NOT_FOUND (err u102)) +(define-constant ERR_INSUFFICIENT_FUNDS (err u103)) +(define-constant ERR_INVALID_STATUS (err u104)) +(define-constant ERR_DEADLINE_PASSED (err u105)) +(define-constant ERR_INVALID_REVIEW (err u106)) +(define-constant ERR_ALREADY_REVIEWED (err u107)) +(define-constant ERR_NOT_ENOUGH_REVIEWS (err u108)) +(define-constant ERR_INVALID_DEADLINE (err u109)) +(define-constant ERR_INVALID_MILESTONES (err u110)) +(define-constant ERR_INSUFFICIENT_REPUTATION (err u111)) +(define-constant ERR_ACTIVE_PROPOSAL_EXISTS (err u112)) +(define-constant ERR_EVENT_EMISSION_FAILED (err u113)) +(define-constant ERR_INVALID_MILESTONE_INDEX (err u114)) +(define-constant ERR_MAP_UPDATE_FAILED (err u115)) +(define-constant MAX_EVENT_LENGTH u500) +(define-constant MAX_DESCRIPTION_LENGTH u500) +(define-constant ERR_INVALID_MILESTONE (err u116)) + +;; Define data maps +(define-map Proposals + { proposal-id: uint } + { + researcher: principal, + title: (string-ascii 100), + description: (string-utf8 1000), + requested-amount: uint, + status: (string-ascii 20), + funded-amount: uint, + milestones: (list 5 (string-ascii 100)), + deadline: uint, + review-count: uint, + average-rating: uint, + escrow-amount: uint + } +) + +(define-map ResearcherBalance principal uint) +(define-map ResearcherReputation principal uint) +(define-map Reviews { proposal-id: uint, reviewer: principal } { rating: uint, comment: (string-utf8 500) }) +(define-map Votes { proposal-id: uint, voter: principal } uint) +(define-map ActiveResearcherProposals principal uint) + +(define-map Events + { event-id: uint } + { + event-type: (string-ascii 20), + proposal-id: uint, + data: (string-utf8 500) + } +) + +;; Define variables +(define-data-var proposal-count uint u0) +(define-data-var total-funds uint u0) +(define-data-var min-reputation-for-proposal uint u10) +(define-data-var last-event-id uint u0) + +;; Helper function +(define-private (update-proposal (proposal-id uint) (proposal {researcher: principal, title: (string-ascii 100), description: (string-utf8 1000), requested-amount: uint, status: (string-ascii 20), funded-amount: uint, milestones: (list 5 (string-ascii 100)), deadline: uint, review-count: uint, average-rating: uint, escrow-amount: uint})) + (if (map-set Proposals { proposal-id: proposal-id } proposal) + (ok true) + (err ERR_MAP_UPDATE_FAILED))) + +(define-private (update-milestone-at-index + (milestones (list 5 (string-ascii 100))) + (index uint) + (new-milestone (string-ascii 100)) +) + (let + ((len (len milestones))) + (asserts! (< index len) ERR_INVALID_MILESTONE_INDEX) + (ok (get result (fold update-milestone-fold + milestones + { + current-index: u0, + target-index: index, + new-milestone: new-milestone, + result: (list) + } + ))) + ) +) + +(define-private (update-milestone-fold + (milestone (string-ascii 100)) + (state { + current-index: uint, + target-index: uint, + new-milestone: (string-ascii 100), + result: (list 5 (string-ascii 100)) + }) +) + (let + ( + (updated-result (unwrap-panic (as-max-len? + (append (get result state) + (if (is-eq (get current-index state) (get target-index state)) + (get new-milestone state) + milestone)) + u5))) + ) + (merge state { + current-index: (+ (get current-index state) u1), + result: updated-result + }) + ) +) + +;; Private functions + +(define-private (emit-event (event-type (string-ascii 20)) (proposal-id uint) (data (string-utf8 500))) + (let + ((event-id (+ (var-get last-event-id) u1)) + (truncated-data (unwrap-panic (as-max-len? data u500)))) + (if (map-set Events + { event-id: event-id } + { + event-type: event-type, + proposal-id: proposal-id, + data: truncated-data + }) + (begin + (var-set last-event-id event-id) + (ok event-id)) + (err ERR_EVENT_EMISSION_FAILED)))) + +;; Public functions + +(define-public (submit-proposal (title (string-ascii 100)) (description (string-utf8 1000)) (requested-amount uint) (milestones (list 5 (string-ascii 100))) (deadline uint)) + (let + ( + (proposal-id (+ (var-get proposal-count) u1)) + (researcher-reputation (default-to u0 (map-get? ResearcherReputation tx-sender))) + (truncated-description (unwrap-panic (as-max-len? description u500))) + ) + (asserts! (> requested-amount u0) ERR_INVALID_AMOUNT) + (asserts! (> deadline block-height) ERR_INVALID_DEADLINE) + (asserts! (> (len milestones) u0) ERR_INVALID_MILESTONES) + (asserts! (>= researcher-reputation (var-get min-reputation-for-proposal)) ERR_INSUFFICIENT_REPUTATION) + (asserts! (is-none (map-get? ActiveResearcherProposals tx-sender)) ERR_ACTIVE_PROPOSAL_EXISTS) + + (match (update-proposal proposal-id + { + researcher: tx-sender, + title: title, + description: description, + requested-amount: requested-amount, + status: "pending", + funded-amount: u0, + milestones: milestones, + deadline: deadline, + review-count: u0, + average-rating: u0, + escrow-amount: u0 + }) + update-success (begin + (var-set proposal-count proposal-id) + (map-set ActiveResearcherProposals tx-sender proposal-id) + (match (emit-event "proposal-submitted" proposal-id truncated-description) + emit-success (ok proposal-id) + emit-error emit-error)) + update-error update-error)) +) + +;; Public functions + +(define-public (fund-proposal (proposal-id uint) (amount uint)) + (let + ( + (proposal (unwrap! (map-get? Proposals { proposal-id: proposal-id }) (err ERR_PROPOSAL_NOT_FOUND))) + (current-balance (stx-get-balance tx-sender)) + ) + (asserts! (>= current-balance amount) (err ERR_INSUFFICIENT_FUNDS)) + (asserts! (is-eq (get status proposal) "approved") (err ERR_INVALID_STATUS)) + (asserts! (<= (get deadline proposal) block-height) (err ERR_DEADLINE_PASSED)) + (match (stx-transfer? amount tx-sender (as-contract tx-sender)) + success + (let + ((new-funded-amount (+ (get funded-amount proposal) amount)) + (new-status (if (>= new-funded-amount (get requested-amount proposal)) + "funded" + "partially-funded"))) + (var-set total-funds (+ (var-get total-funds) amount)) + (match (update-proposal proposal-id + (merge proposal { + status: new-status, + funded-amount: new-funded-amount, + escrow-amount: (+ (get escrow-amount proposal) amount) + })) + update-success + (let + ((researcher-balance (default-to u0 (map-get? ResearcherBalance (get researcher proposal))))) + (if (map-set ResearcherBalance + (get researcher proposal) + (+ researcher-balance amount)) + (match (emit-event "proposal-funded" proposal-id u"Proposal funded") + emit-success (ok true) + emit-error (err ERR_EVENT_EMISSION_FAILED)) + (err ERR_MAP_UPDATE_FAILED))) + update-error (err ERR_MAP_UPDATE_FAILED))) + error (err ERR_INSUFFICIENT_FUNDS)) + ) +) + +(define-public (approve-proposal (proposal-id uint)) + (let + ((proposal (unwrap! (map-get? Proposals { proposal-id: proposal-id }) (err ERR_PROPOSAL_NOT_FOUND)))) + (asserts! (is-eq tx-sender CONTRACT_OWNER) (err ERR_NOT_AUTHORIZED)) + (asserts! (is-eq (get status proposal) "pending") (err ERR_INVALID_STATUS)) + (match (update-proposal proposal-id + (merge proposal { status: "approved" })) + update-success + (match (emit-event "proposal-approved" proposal-id u"Proposal approved") + emit-success (ok true) + emit-error (err ERR_EVENT_EMISSION_FAILED)) + update-error (err ERR_MAP_UPDATE_FAILED)) + ) +) + +(define-public (submit-review (proposal-id uint) (rating uint) (comment (string-utf8 500))) + (let + ((proposal (unwrap! (map-get? Proposals { proposal-id: proposal-id }) (err ERR_PROPOSAL_NOT_FOUND))) + (existing-review (map-get? Reviews { proposal-id: proposal-id, reviewer: tx-sender }))) + (asserts! (and (>= rating u1) (<= rating u5)) (err ERR_INVALID_REVIEW)) + (asserts! (is-none existing-review) (err ERR_ALREADY_REVIEWED)) + (if (map-set Reviews + { proposal-id: proposal-id, reviewer: tx-sender } + { rating: rating, comment: comment }) + (let + ((new-review-count (+ (get review-count proposal) u1)) + (new-average-rating (/ (+ (* (get average-rating proposal) (get review-count proposal)) rating) new-review-count))) + (if (map-set Proposals + { proposal-id: proposal-id } + (merge proposal { + review-count: new-review-count, + average-rating: new-average-rating + })) + (match (emit-event "review-submitted" proposal-id comment) + success (ok true) + error (err ERR_EVENT_EMISSION_FAILED)) + (err ERR_MAP_UPDATE_FAILED))) + (err ERR_MAP_UPDATE_FAILED)) + ) +) + +(define-public (release-funds (proposal-id uint)) + (let + ((proposal (unwrap! (map-get? Proposals { proposal-id: proposal-id }) (err ERR_PROPOSAL_NOT_FOUND)))) + (asserts! (is-eq tx-sender CONTRACT_OWNER) (err ERR_NOT_AUTHORIZED)) + (asserts! (is-eq (get status proposal) "funded") (err ERR_INVALID_STATUS)) + (asserts! (>= (get review-count proposal) u3) (err ERR_NOT_ENOUGH_REVIEWS)) + (asserts! (>= (get average-rating proposal) u4) (err ERR_INVALID_REVIEW)) + (match (as-contract (stx-transfer? (get escrow-amount proposal) tx-sender (get researcher proposal))) + transfer-result + (if (map-set Proposals + { proposal-id: proposal-id } + (merge proposal { + status: "completed", + escrow-amount: u0 + })) + (if (map-set ResearcherReputation + (get researcher proposal) + (+ (default-to u0 (map-get? ResearcherReputation (get researcher proposal))) u1)) + (match (emit-event "funds-released" proposal-id u"Funds released to researcher") + event-result (ok true) + event-error (err ERR_EVENT_EMISSION_FAILED)) + (err ERR_MAP_UPDATE_FAILED)) + (err ERR_MAP_UPDATE_FAILED)) + transfer-error (err ERR_INSUFFICIENT_FUNDS)) + ) +) + +(define-public (update-milestone (proposal-id uint) (milestone-index uint) (new-milestone (string-ascii 100))) + (let + ((proposal (unwrap! (map-get? Proposals { proposal-id: proposal-id }) ERR_PROPOSAL_NOT_FOUND)) + (event-data (concat "Milestone updated: " new-milestone))) + (asserts! (is-eq tx-sender (get researcher proposal)) ERR_NOT_AUTHORIZED) + (asserts! (is-eq (get status proposal) "approved") ERR_INVALID_STATUS) + (match (update-milestone-at-index (get milestones proposal) milestone-index new-milestone) + updated-milestones + (if (map-set Proposals + { proposal-id: proposal-id } + (merge proposal { milestones: updated-milestones })) + (match (emit-event "milestone-updated" proposal-id + (unwrap! (as-max-len? (utf8 event-data) u500) ERR_INVALID_MILESTONE)) + event-success (ok true) + event-error (err ERR_EVENT_EMISSION_FAILED)) + (err ERR_MAP_UPDATE_FAILED)) + error error) + ) +) + +(define-public (set-min-reputation (new-min-reputation uint)) + (begin + (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED) + (var-set min-reputation-for-proposal new-min-reputation) + (ok true) + ) +) \ No newline at end of file diff --git a/Research/settings/Devnet.toml b/Research/settings/Devnet.toml new file mode 100644 index 0000000..4b94b79 --- /dev/null +++ b/Research/settings/Devnet.toml @@ -0,0 +1,126 @@ +[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.wallet_9] +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_bitcoin_explorer = true +# working_dir = "tmp/devnet" +# stacks_node_events_observers = ["host.docker.internal:8002"] +# miner_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" +# miner_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:devnet-v2" +# stacks_node_image_url = "localhost:5000/stacks-node:devnet-v2" +# stacks_api_image_url = "blockstack/stacks-blockchain-api:latest" +# stacks_explorer_image_url = "blockstack/explorer:latest" +# bitcoin_explorer_image_url = "quay.io/hirosystems/bitcoin-explorer:devnet" +# postgres_image_url = "postgres:alpine" + +# Send some stacking orders +[[devnet.pox_stacking_orders]] +start_at_cycle = 3 +duration = 12 +wallet = "wallet_1" +slots = 2 +btc_address = "mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC" + +[[devnet.pox_stacking_orders]] +start_at_cycle = 3 +duration = 12 +wallet = "wallet_2" +slots = 1 +btc_address = "muYdXKmX9bByAueDe6KFfHd5Ff1gdN9ErG" + +[[devnet.pox_stacking_orders]] +start_at_cycle = 3 +duration = 12 +wallet = "wallet_3" +slots = 1 +btc_address = "mvZtbibDAAA3WLpY7zXXFqRa3T4XSknBX7" diff --git a/Research/tests/academic-research_test.ts b/Research/tests/academic-research_test.ts new file mode 100644 index 0000000..561c544 --- /dev/null +++ b/Research/tests/academic-research_test.ts @@ -0,0 +1,26 @@ + +import { Clarinet, Tx, Chain, Account, types } from 'https://deno.land/x/clarinet@v0.31.0/index.ts'; +import { assertEquals } from 'https://deno.land/std@0.90.0/testing/asserts.ts'; + +Clarinet.test({ + name: "Ensure that <...>", + async fn(chain: Chain, accounts: Map) { + let block = chain.mineBlock([ + /* + * Add transactions with: + * Tx.contractCall(...) + */ + ]); + assertEquals(block.receipts.length, 0); + assertEquals(block.height, 2); + + block = chain.mineBlock([ + /* + * Add transactions with: + * Tx.contractCall(...) + */ + ]); + assertEquals(block.receipts.length, 0); + assertEquals(block.height, 3); + }, +});