diff --git a/draw/.gitignore b/draw/.gitignore new file mode 100644 index 0000000..a56ad9b --- /dev/null +++ b/draw/.gitignore @@ -0,0 +1,5 @@ + +**/settings/Mainnet.toml +**/settings/Testnet.toml +.requirements/ +history.txt diff --git a/draw/.vscode/settings.json b/draw/.vscode/settings.json new file mode 100644 index 0000000..94da4be --- /dev/null +++ b/draw/.vscode/settings.json @@ -0,0 +1,5 @@ + +{ + "deno.enable": true, + "files.eol": "\n" +} diff --git a/draw/.vscode/tasks.json b/draw/.vscode/tasks.json new file mode 100644 index 0000000..22af91c --- /dev/null +++ b/draw/.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/draw/Clarinet.toml b/draw/Clarinet.toml new file mode 100644 index 0000000..7cfd88d --- /dev/null +++ b/draw/Clarinet.toml @@ -0,0 +1,23 @@ +[project] +name = "draw" +authors = [] +description = "" +telemetry = true +requirements = [] +cache_dir = "/home/runner/Toyin/Great-Height/draw/./.requirements" +boot_contracts = ["pox", "costs-v2", "bns"] +[contracts.raffle] +path = "contracts/raffle.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/draw/README.md b/draw/README.md new file mode 100644 index 0000000..5716f7a --- /dev/null +++ b/draw/README.md @@ -0,0 +1,79 @@ +# Raffle Smart Contract + +## About + +This smart contract implements a decentralized raffle system on the Stacks blockchain. It allows for the creation and management of raffles, ticket purchases, winner selection, and prize distribution. + +## Features + +•⁠ ⁠Raffle initialization with customizable parameters +•⁠ ⁠Ticket purchasing with limits +•⁠ ⁠Random winner selection +•⁠ ⁠Prize claiming +•⁠ ⁠Fee collection for the contract owner +•⁠ ⁠Raffle cancellation and refund functionality + +## Contract Details + +### Constants + +•⁠ ⁠⁠ CONTRACT-OWNER ⁠: The owner of the contract (set to the contract deployer) +•⁠ ⁠Error codes for various scenarios (e.g., unauthorized access, insufficient balance) + +### Data Variables + +•⁠ ⁠⁠ raffle-state ⁠: Boolean indicating if a raffle is active +•⁠ ⁠⁠ ticket-cost ⁠: Cost of a single ticket (in microSTX) +•⁠ ⁠⁠ raffle-end-block ⁠: Block height at which the raffle ends +•⁠ ⁠⁠ winning-participant ⁠: The address of the winning participant +•⁠ ⁠⁠ prize-claim-status ⁠: Boolean indicating if the prize has been claimed +•⁠ ⁠⁠ minimum-participants ⁠: Minimum number of participants required for a valid raffle +•⁠ ⁠⁠ max-tickets-per-participant ⁠: Maximum number of tickets a single participant can purchase +•⁠ ⁠⁠ raffle-fee-rate ⁠: Percentage of the total pool taken as a fee (e.g., 5%) + +### Maps + +•⁠ ⁠⁠ participant-tickets ⁠: Tracks the number of tickets purchased by each participant +•⁠ ⁠⁠ participant-registry ⁠: Maps participant indices to their addresses +•⁠ ⁠⁠ participant-indices ⁠: Maps participant addresses to their indices + +### Main Functions + +1.⁠ ⁠⁠ initialize-raffle ⁠: Starts a new raffle with specified parameters +2.⁠ ⁠⁠ purchase-tickets ⁠: Allows participants to buy raffle tickets +3.⁠ ⁠⁠ conclude-raffle ⁠: Ends the raffle and selects a winner +4.⁠ ⁠⁠ claim-raffle-prize ⁠: Allows the winner to claim their prize +5.⁠ ⁠⁠ withdraw-raffle-fees ⁠: Enables the contract owner to withdraw collected fees +6.⁠ ⁠⁠ cancel-active-raffle ⁠: Cancels an active raffle if minimum participants aren't met +7.⁠ ⁠⁠ refund-participant-tickets ⁠: Refunds tickets if a raffle is cancelled + +### Read-Only Functions + +•⁠ ⁠⁠ get-ticket-price ⁠: Returns the current ticket price +•⁠ ⁠⁠ get-raffle-info ⁠: Provides information about the current raffle state +•⁠ ⁠⁠ get-participant-tickets ⁠: Returns the number of tickets owned by a participant +•⁠ ⁠⁠ get-winning-participant ⁠: Returns the address of the winning participant +•⁠ ⁠⁠ get-prize-info ⁠: Provides information about the prize status and total pool + +## Usage + +1.⁠ ⁠Deploy the contract to the Stacks blockchain. +2.⁠ ⁠The contract owner initializes a raffle using ⁠ initialize-raffle ⁠. +3.⁠ ⁠Participants purchase tickets using ⁠ purchase-tickets ⁠. +4.⁠ ⁠Once the raffle end block is reached, anyone can call ⁠ conclude-raffle ⁠ to select a winner. +5.⁠ ⁠The winner can claim their prize using ⁠ claim-raffle-prize ⁠. +6.⁠ ⁠The contract owner can withdraw fees using ⁠ withdraw-raffle-fees ⁠. + +## Security Considerations + +•⁠ ⁠The contract uses block height for randomness, which isn't cryptographically secure. In a production environment, consider using a more robust randomness source. +•⁠ ⁠There's no mechanism to update the contract owner. Consider implementing an ownership transfer function if needed. +•⁠ ⁠Ensure proper testing and auditing before deploying to mainnet. + +## Development and Testing + +To interact with and test this contract: + +1.⁠ ⁠Use the [Clarinet](https://github.com/hirosystems/clarinet) development tool for local testing. +2.⁠ ⁠Deploy to testnet for further testing before mainnet deployment. +3.⁠ ⁠Use [Stacks.js](https://github.com/hirosystems/stacks.js) or other Stacks libraries to interact with the deployed contract. \ No newline at end of file diff --git a/draw/contracts/raffle.clar b/draw/contracts/raffle.clar new file mode 100644 index 0000000..d13fe09 --- /dev/null +++ b/draw/contracts/raffle.clar @@ -0,0 +1,225 @@ +;; Enhanced Raffle Smart Contract with Improved Variable Names and Updated Constants + +;; Constants +(define-constant CONTRACT-OWNER tx-sender) +(define-constant ERR-UNAUTHORIZED (err u100)) +(define-constant ERR-RAFFLE-INACTIVE (err u101)) +(define-constant ERR-RAFFLE-IN-PROGRESS (err u102)) +(define-constant ERR-INSUFFICIENT-BALANCE (err u103)) +(define-constant ERR-NO-PARTICIPANTS (err u104)) +(define-constant ERR-RAFFLE-ENDED (err u105)) +(define-constant ERR-INVALID-INPUT (err u106)) +(define-constant ERR-NOT-WINNER (err u107)) +(define-constant ERR-PRIZE-ALREADY-CLAIMED (err u108)) + +;; Data Variables +(define-data-var raffle-state bool false) +(define-data-var ticket-cost uint u1000000) ;; 1 STX +(define-data-var raffle-end-block uint u0) +(define-data-var winning-participant (optional principal) none) +(define-data-var prize-claim-status bool false) +(define-data-var minimum-participants uint u5) +(define-data-var max-tickets-per-participant uint u10) +(define-data-var raffle-fee-rate uint u5) ;; 5% fee + +;; Maps +(define-map participant-tickets principal uint) +(define-map participant-registry {index: uint} {address: principal}) +(define-map participant-indices principal uint) + +;; Variables +(define-data-var participant-count uint u0) +(define-data-var total-tickets-sold uint u0) + +;; Private Functions +(define-private (is-owner) + (is-eq tx-sender CONTRACT-OWNER) +) + +(define-private (is-raffle-active) + (var-get raffle-state) +) + +(define-private (register-new-participant (address principal)) + (let ( + (current-count (var-get participant-count)) + (new-count (+ current-count u1)) + ) + (map-insert participant-registry {index: new-count} {address: address}) + (map-insert participant-indices address new-count) + (var-set participant-count new-count) + ) +) + +(define-private (generate-random-number (seed uint)) + (let ( + (combined-value (+ (var-get participant-count) seed block-height)) + (random-value (mod combined-value (var-get participant-count))) + ) + (if (is-eq random-value u0) + u1 + (+ random-value u1) + ) + ) +) + +(define-private (calculate-raffle-fee (amount uint)) + (/ (* amount (var-get raffle-fee-rate)) u100) +) + +;; Public Functions +(define-public (initialize-raffle (duration uint) (price uint) (min-participants uint) (max-tickets uint)) + (begin + (asserts! (is-owner) ERR-UNAUTHORIZED) + (asserts! (not (is-raffle-active)) ERR-RAFFLE-IN-PROGRESS) + (asserts! (> duration u0) ERR-INVALID-INPUT) + (asserts! (> price u0) ERR-INVALID-INPUT) + (asserts! (>= min-participants u2) ERR-INVALID-INPUT) + (asserts! (> max-tickets u0) ERR-INVALID-INPUT) + (var-set raffle-state true) + (var-set ticket-cost price) + (var-set raffle-end-block (+ block-height duration)) + (var-set minimum-participants min-participants) + (var-set max-tickets-per-participant max-tickets) + (var-set prize-claim-status false) + (var-set participant-count u0) + (var-set total-tickets-sold u0) + (ok true) + ) +) + +(define-public (purchase-tickets (quantity uint)) + (let ( + (buyer tx-sender) + (price (var-get ticket-cost)) + (total-purchase-cost (* price quantity)) + (current-ticket-count (default-to u0 (map-get? participant-tickets buyer))) + (new-ticket-count (+ current-ticket-count quantity)) + ) + (asserts! (is-raffle-active) ERR-RAFFLE-INACTIVE) + (asserts! (<= block-height (var-get raffle-end-block)) ERR-RAFFLE-ENDED) + (asserts! (>= (stx-get-balance buyer) total-purchase-cost) ERR-INSUFFICIENT-BALANCE) + (asserts! (<= new-ticket-count (var-get max-tickets-per-participant)) ERR-INVALID-INPUT) + (try! (stx-transfer? total-purchase-cost buyer (as-contract tx-sender))) + (map-set participant-tickets buyer new-ticket-count) + (match (map-get? participant-indices buyer) + index true + (register-new-participant buyer) + ) + (var-set total-tickets-sold (+ (var-get total-tickets-sold) quantity)) + (ok new-ticket-count) + ) +) + +(define-public (conclude-raffle) + (begin + (asserts! (is-owner) ERR-UNAUTHORIZED) + (asserts! (is-raffle-active) ERR-RAFFLE-INACTIVE) + (asserts! (>= block-height (var-get raffle-end-block)) ERR-RAFFLE-INACTIVE) + (let ( + (total-participants (var-get participant-count)) + (random-seed block-height) + ) + (asserts! (>= total-participants (var-get minimum-participants)) ERR-NO-PARTICIPANTS) + (let ( + (winner-index (generate-random-number random-seed)) + (winner-info (unwrap! (map-get? participant-registry {index: winner-index}) ERR-NO-PARTICIPANTS)) + ) + (var-set winning-participant (some (get address winner-info))) + (var-set raffle-state false) + (ok (get address winner-info)) + ) + ) + ) +) + +(define-public (claim-raffle-prize) + (let ( + (claimer tx-sender) + (winner (unwrap! (var-get winning-participant) ERR-NO-PARTICIPANTS)) + ) + (asserts! (is-eq claimer winner) ERR-NOT-WINNER) + (asserts! (not (var-get prize-claim-status)) ERR-PRIZE-ALREADY-CLAIMED) + (let ( + (total-prize-pool (var-get total-tickets-sold)) + (raffle-fee (calculate-raffle-fee total-prize-pool)) + (winner-prize (- total-prize-pool raffle-fee)) + ) + (try! (as-contract (stx-transfer? winner-prize tx-sender winner))) + (var-set prize-claim-status true) + (ok winner-prize) + ) + ) +) + +(define-read-only (get-ticket-price) + (ok (var-get ticket-cost)) +) + +(define-read-only (get-raffle-info) + (ok { + active: (var-get raffle-state), + end-block: (var-get raffle-end-block), + current-block: block-height, + total-tickets: (var-get total-tickets-sold), + participant-count: (var-get participant-count), + minimum-participants: (var-get minimum-participants), + max-tickets-per-participant: (var-get max-tickets-per-participant) + }) +) + +(define-read-only (get-participant-tickets (address principal)) + (ok (default-to u0 (map-get? participant-tickets address))) +) + +(define-read-only (get-winning-participant) + (ok (var-get winning-participant)) +) + +(define-read-only (get-prize-info) + (ok { + claimed: (var-get prize-claim-status), + total-prize-pool: (var-get total-tickets-sold) + }) +) + +(define-public (withdraw-raffle-fees) + (begin + (asserts! (is-owner) ERR-UNAUTHORIZED) + (asserts! (not (is-raffle-active)) ERR-RAFFLE-INACTIVE) + (asserts! (var-get prize-claim-status) ERR-PRIZE-ALREADY-CLAIMED) + (let ( + (total-sales (var-get total-tickets-sold)) + (fee (calculate-raffle-fee total-sales)) + ) + (try! (as-contract (stx-transfer? fee tx-sender CONTRACT-OWNER))) + (ok fee) + ) + ) +) + +(define-public (cancel-active-raffle) + (begin + (asserts! (is-owner) ERR-UNAUTHORIZED) + (asserts! (is-raffle-active) ERR-RAFFLE-INACTIVE) + (asserts! (< (var-get participant-count) (var-get minimum-participants)) ERR-INVALID-INPUT) + (var-set raffle-state false) + (ok true) + ) +) + +(define-public (refund-participant-tickets) + (let ( + (participant tx-sender) + (ticket-count (default-to u0 (map-get? participant-tickets participant))) + (refund-amount (* ticket-count (var-get ticket-cost))) + ) + (asserts! (not (is-raffle-active)) ERR-RAFFLE-IN-PROGRESS) + (asserts! (> ticket-count u0) ERR-INVALID-INPUT) + (try! (as-contract (stx-transfer? refund-amount tx-sender participant))) + (map-delete participant-tickets participant) + (map-delete participant-indices participant) + (var-set total-tickets-sold (- (var-get total-tickets-sold) ticket-count)) + (ok refund-amount) + ) +) \ No newline at end of file diff --git a/draw/settings/Devnet.toml b/draw/settings/Devnet.toml new file mode 100644 index 0000000..4b94b79 --- /dev/null +++ b/draw/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/draw/tests/raffle_test.ts b/draw/tests/raffle_test.ts new file mode 100644 index 0000000..561c544 --- /dev/null +++ b/draw/tests/raffle_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); + }, +});