Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions funding/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

settings/Mainnet.toml
settings/Testnet.toml
history.txt
4 changes: 4 additions & 0 deletions funding/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

{
"deno.enable": true,
}
18 changes: 18 additions & 0 deletions funding/.vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
11 changes: 11 additions & 0 deletions funding/Clarinet.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[project]
name = "funding"
authors = []
description = ""
telemetry = true
requirements = []
analysis = ["check_checker"]
costs_version = 2
[contracts.project-funds]
path = "contracts/project-funds.clar"
depends_on = []
192 changes: 192 additions & 0 deletions funding/contracts/project-funds.clar
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
;; Define the contract
(define-data-var contract-admin principal tx-sender)

;; Define constants for errors
(define-constant ERR-UNAUTHORIZED-ACCESS (err u100))
(define-constant ERR-DUPLICATE-ENTRY (err u101))
(define-constant ERR-ENTRY-NOT-FOUND (err u102))
(define-constant ERR-BALANCE-INSUFFICIENT (err u103))
(define-constant ERR-PROJECT-NOT-FOUND (err u104))
(define-constant ERR-EXPENSE-NOT-FOUND (err u105))
(define-constant ERR-INVALID-PARAMETERS (err u106))

;; Define constants for access levels
(define-constant ACCESS-OWNER u1)
(define-constant ACCESS-MANAGER u2)
(define-constant ACCESS-PARTICIPANT u3)

;; Define data maps
(define-map access-levels { account: principal } { level: uint })

(define-map projects
{ id: uint }
{
title: (string-utf8 50),
details: (string-utf8 255),
funding-goal: uint,
funds-raised: uint,
state: (string-ascii 20)
})

(define-map contributions
{ id: uint }
{ contributor: principal, project-id: uint, value: uint, block-time: uint })

(define-map expenditures
{ id: uint }
{
project-id: uint,
phase: uint,
details: (string-utf8 255),
value: uint,
state: (string-ascii 20)
})

;; Define data variables
(define-data-var project-counter uint u0)
(define-data-var contribution-counter uint u0)
(define-data-var expenditure-counter uint u0)

;; Helper functions
(define-private (has-access (account principal) (minimum-level uint))
(let ((access-data (default-to { level: u0 } (map-get? access-levels { account: account }))))
(>= (get level access-data) minimum-level)))

(define-private (get-latest-phase (project-id uint))
(var-get expenditure-counter))

;; Access management functions
(define-public (assign-access (account principal) (new-level uint))
(let ((current-level (default-to u0 (get level (map-get? access-levels { account: account })))))
(if (and
(is-eq tx-sender (var-get contract-admin))
(<= new-level ACCESS-PARTICIPANT)
(not (is-eq account tx-sender)) ;; Ensure account is not setting their own access
(or (is-eq new-level ACCESS-OWNER)
(is-eq new-level ACCESS-MANAGER)
(is-eq new-level ACCESS-PARTICIPANT)))
(ok (map-set access-levels { account: account } { level: new-level }))
ERR-UNAUTHORIZED-ACCESS)))

(define-public (revoke-access (account principal))
(if (and
(is-eq tx-sender (var-get contract-admin))
(is-some (map-get? access-levels { account: account }))
(not (is-eq account tx-sender))) ;; Ensure account is not removing their own access
(ok (map-delete access-levels { account: account }))
ERR-UNAUTHORIZED-ACCESS))

;; Main functions
(define-public (create-project (title (string-utf8 50)) (details (string-utf8 255)) (funding-goal uint))
(let
((project-id (+ (var-get project-counter) u1)))
(if (and (has-access tx-sender ACCESS-MANAGER)
(> (len title) u0)
(> (len details) u0)
(> funding-goal u0))
(begin
(map-set projects
{ id: project-id }
{
title: title,
details: details,
funding-goal: funding-goal,
funds-raised: u0,
state: "active"
})
(var-set project-counter project-id)
(ok project-id))
ERR-INVALID-PARAMETERS)))

(define-read-only (get-project (id uint))
(match (map-get? projects { id: id })
project (ok project)
ERR-PROJECT-NOT-FOUND))

(define-public (fund-project (project-id uint) (value uint))
(let
((project (unwrap! (get-project project-id) ERR-PROJECT-NOT-FOUND)))
(if (and (> value u0)
(< project-id (var-get project-counter)) ;; Check if project-id is valid
(is-some (map-get? projects { id: project-id })))
(match (stx-transfer? value tx-sender (as-contract tx-sender))
success (begin
(map-set projects
{ id: project-id }
(merge project { funds-raised: (+ (get funds-raised project) value) }))
(map-set contributions
{ id: (+ (var-get contribution-counter) u1) }
{ contributor: tx-sender, project-id: project-id, value: value, block-time: block-height })
(var-set contribution-counter (+ (var-get contribution-counter) u1))
(ok true))
error ERR-BALANCE-INSUFFICIENT)
ERR-INVALID-PARAMETERS)))

(define-public (record-expenditure (project-id uint) (details (string-utf8 255)) (value uint))
(let
((project (unwrap! (get-project project-id) ERR-PROJECT-NOT-FOUND)))
(if (and (has-access tx-sender ACCESS-OWNER)
(> (len details) u0)
(> value u0)
(< project-id (var-get project-counter))) ;; Check if project-id is valid
(let
((phase (+ (get-latest-phase project-id) u1))
(expenditure-id (+ (var-get expenditure-counter) u1)))
(begin
(map-set expenditures
{ id: expenditure-id }
{
project-id: project-id,
phase: phase,
details: details,
value: value,
state: "pending"
})
(var-set expenditure-counter expenditure-id)
(ok phase)))
ERR-INVALID-PARAMETERS)))

(define-public (validate-expenditure (project-id uint) (phase uint))
(let
((expenditure-entry (unwrap! (map-get? expenditures { id: phase }) ERR-EXPENSE-NOT-FOUND))
(project (unwrap! (get-project project-id) ERR-PROJECT-NOT-FOUND)))
(if (and (has-access tx-sender ACCESS-OWNER)
(is-eq (get project-id expenditure-entry) project-id)
(< project-id (var-get project-counter)) ;; Check if project-id is valid
(< phase (var-get expenditure-counter))) ;; Check if phase is valid
(if (<= (get value expenditure-entry) (get funds-raised project))
(begin
(map-set expenditures
{ id: phase }
(merge expenditure-entry { state: "approved" }))
(ok true))
ERR-BALANCE-INSUFFICIENT)
ERR-UNAUTHORIZED-ACCESS)))

;; Get a single contribution by ID
(define-read-only (get-contribution-by-id (contribution-id uint))
(match (map-get? contributions { id: contribution-id })
contribution (ok contribution)
ERR-ENTRY-NOT-FOUND))

;; Get a single expenditure entry by ID
(define-read-only (get-expenditure-by-id (expenditure-id uint))
(match (map-get? expenditures { id: expenditure-id })
expense (ok expense)
ERR-ENTRY-NOT-FOUND))

;; Get the total number of contributions
(define-read-only (get-contribution-total)
(ok (var-get contribution-counter)))

;; Get the total number of expenditure entries
(define-read-only (get-expenditure-total)
(ok (var-get expenditure-counter)))

;; Contract initialization
(define-private (setup-contract)
(begin
(map-set access-levels { account: tx-sender } { level: ACCESS-OWNER })
(var-set contract-admin tx-sender)))

(setup-contract)
127 changes: 127 additions & 0 deletions funding/settings/Devnet.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
[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_bitcoin_explorer = true
# disable_stacks_explorer = true
# disable_stacks_api = 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_port = 18442
# 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"
# stacks_node_image_url = "localhost:5000/stacks-node:devnet"
# 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"
26 changes: 26 additions & 0 deletions funding/tests/project-funds_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

import { Clarinet, Tx, Chain, Account, types } from 'https://deno.land/x/clarinet@v0.14.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<string, Account>) {
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);
},
});