diff --git a/predict/.gitignore b/predict/.gitignore new file mode 100644 index 0000000..a56ad9b --- /dev/null +++ b/predict/.gitignore @@ -0,0 +1,5 @@ + +**/settings/Mainnet.toml +**/settings/Testnet.toml +.requirements/ +history.txt diff --git a/predict/.vscode/settings.json b/predict/.vscode/settings.json new file mode 100644 index 0000000..94da4be --- /dev/null +++ b/predict/.vscode/settings.json @@ -0,0 +1,5 @@ + +{ + "deno.enable": true, + "files.eol": "\n" +} diff --git a/predict/.vscode/tasks.json b/predict/.vscode/tasks.json new file mode 100644 index 0000000..22af91c --- /dev/null +++ b/predict/.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/predict/Clarinet.toml b/predict/Clarinet.toml new file mode 100644 index 0000000..1dd1817 --- /dev/null +++ b/predict/Clarinet.toml @@ -0,0 +1,23 @@ +[project] +name = "predict" +authors = [] +description = "" +telemetry = true +requirements = [] +cache_dir = "/home/runner/hadassah/High-level/predict/./.requirements" +boot_contracts = ["pox", "costs-v2", "bns"] +[contracts.prediction] +path = "contracts/prediction.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/predict/README.md b/predict/README.md new file mode 100644 index 0000000..43420b1 --- /dev/null +++ b/predict/README.md @@ -0,0 +1,128 @@ +# Prediction Market Smart Contract + +## ABOUT + +This smart contract implements a decentralized prediction market on the Stacks blockchain. Users can create markets, place bets, resolve markets, and claim payouts. The contract is designed to be fair, transparent, and efficient. + +## Features + +- Create prediction markets +- Place bets on market outcomes (Yes/No) +- Resolve markets +- Claim payouts for winning bets +- Fee system for contract sustainability + +## Contract Details + +- *Language*: Clarity +- *Blockchain*: Stacks +- *Minimum Bet*: 1 STX (1,000,000 microSTX) +- *Fee*: 0.5% of winnings + +## Functions + +### Public Functions + +1. create-market: Create a new prediction market +2. place-bet: Place a bet on a market outcome +3. resolve-market: Resolve a market (only by market creator) +4. claim-payout: Claim winnings from a resolved market +5. withdraw-fees: Withdraw accumulated fees (only by contract owner) + +### Read-only Functions + +1. get-market-info: Get information about a specific market +2. get-user-bets: Get a user's bets for a specific market +3. get-accumulated-fees: Get the total accumulated fees + +## Usage + +### Creating a Market + +To create a new prediction market: + +clarity +(contract-call? .prediction-market create-market "Will it rain tomorrow?" u12345678) + + +- Parameters: + - Description (string-utf8 256): Market question or description + - Resolution timestamp (uint): Block height when the market will be resolved + +### Placing a Bet + +To place a bet on a market outcome: + +clarity +(contract-call? .prediction-market place-bet u1 true u1000000) + + +- Parameters: + - Market ID (uint): The ID of the market + - Bet outcome (bool): true for "Yes", false for "No" + - Amount (uint): Bet amount in microSTX (minimum 1,000,000) + +### Resolving a Market + +To resolve a market (only by the market creator): + +clarity +(contract-call? .prediction-market resolve-market u1 true) + + +- Parameters: + - Market ID (uint): The ID of the market + - Outcome (bool): true for "Yes", false for "No" + +### Claiming a Payout + +To claim a payout for a winning bet: + +clarity +(contract-call? .prediction-market claim-payout u1) + + +- Parameters: + - Market ID (uint): The ID of the resolved market + +### Withdrawing Fees + +To withdraw accumulated fees (only by contract owner): + +clarity +(contract-call? .prediction-market withdraw-fees) + + +## Error Codes + +- ERR-UNAUTHORIZED (u100): Unauthorized action +- ERR-ALREADY-INITIALIZED (u101): Market already initialized +- ERR-NOT-INITIALIZED (u102): Market not initialized +- ERR-INVALID-BET (u103): Invalid bet parameters +- ERR-MARKET-CLOSED (u104): Market is closed for betting +- ERR-ALREADY-RESOLVED (u105): Market already resolved +- ERR-INSUFFICIENT-BALANCE (u106): Insufficient balance for bet +- ERR-NO-PAYOUT (u107): No payout available + +## Security Considerations + +- Only the market creator can resolve a market +- Markets can only be resolved after the specified resolution timestamp +- Minimum bet amount and description length to prevent spam +- Fee system to incentivize contract maintenance and prevent abuse + +## Limitations + +- Binary outcomes only (Yes/No) +- No partial withdrawals or bet cancellations +- Fixed fee percentage + +## Future Improvements + +- Multiple outcome support +- Dynamic fee adjustment +- Partial bet withdrawal +- Integration with external data sources for automated resolution + +## AUTHOR +ESTHER OJO \ No newline at end of file diff --git a/predict/contracts/prediction.clar b/predict/contracts/prediction.clar new file mode 100644 index 0000000..38f9c21 --- /dev/null +++ b/predict/contracts/prediction.clar @@ -0,0 +1,188 @@ +;; Prediction Market Smart Contract + +;; Constants +(define-constant CONTRACT-OWNER tx-sender) +(define-constant ERR-UNAUTHORIZED (err u100)) +(define-constant ERR-ALREADY-INITIALIZED (err u101)) +(define-constant ERR-NOT-INITIALIZED (err u102)) +(define-constant ERR-INVALID-BET (err u103)) +(define-constant ERR-MARKET-CLOSED (err u104)) +(define-constant ERR-ALREADY-RESOLVED (err u105)) +(define-constant ERR-INSUFFICIENT-BALANCE (err u106)) +(define-constant ERR-NO-PAYOUT (err u107)) +(define-constant MINIMUM-BET-AMOUNT u1000000) ;; 1 STX +(define-constant FEE-PERCENTAGE u5) ;; 0.5% +(define-constant MIN-DESCRIPTION-LENGTH u10) + +;; Data Variables +(define-data-var market-state (string-ascii 20) "uninitialized") +(define-data-var market-id uint u0) +(define-data-var market-creator principal CONTRACT-OWNER) +(define-data-var market-description (string-utf8 256) u"") +(define-data-var resolution-timestamp uint u0) +(define-data-var total-yes-amount uint u0) +(define-data-var total-no-amount uint u0) +(define-data-var market-outcome (optional bool) none) +(define-data-var accumulated-fees uint u0) + +;; Data Maps +(define-map markets uint { + creator: principal, + description: (string-utf8 256), + resolution-timestamp: uint, + total-yes-amount: uint, + total-no-amount: uint, + outcome: (optional bool), + state: (string-ascii 20) +}) + +(define-map user-bets { market-id: uint, user: principal } { yes-amount: uint, no-amount: uint }) + +;; Private Functions +(define-private (is-valid-description (description (string-utf8 256))) + (>= (len description) MIN-DESCRIPTION-LENGTH) +) + +(define-private (is-market-open (market-id-param uint)) + (let ((market (unwrap! (map-get? markets market-id-param) false))) + (and + (is-eq (get state market) "active") + (< block-height (get resolution-timestamp market)) + ) + ) +) + +(define-private (calculate-payout (bet-amount uint) (winning-pool uint) (losing-pool uint)) + (let ( + (total-pool (+ winning-pool losing-pool)) + (payout-ratio (/ (* bet-amount u100000000) winning-pool)) + (gross-payout (/ (* total-pool payout-ratio) u100000000)) + (fee (/ (* gross-payout FEE-PERCENTAGE) u1000)) + ) + (- gross-payout fee) + ) +) + +(define-private (transfer-token (amount uint) (sender principal) (recipient principal)) + (begin + (try! (stx-transfer? amount sender recipient)) + (ok true) + ) +) + +;; Public Functions +(define-public (create-market (description (string-utf8 256)) (resolution-time uint)) + (let ((new-market-id (+ (var-get market-id) u1))) + (asserts! (> resolution-time block-height) ERR-INVALID-BET) + (asserts! (is-valid-description description) ERR-INVALID-BET) + (map-set markets new-market-id { + creator: tx-sender, + description: description, + resolution-timestamp: resolution-time, + total-yes-amount: u0, + total-no-amount: u0, + outcome: none, + state: "active" + }) + (var-set market-id new-market-id) + (ok new-market-id) + ) +) + +(define-public (place-bet (market-id-param uint) (bet-yes bool) (amount uint)) + (let ( + (market (unwrap! (map-get? markets market-id-param) ERR-NOT-INITIALIZED)) + (user-bet (default-to { yes-amount: u0, no-amount: u0 } + (map-get? user-bets { market-id: market-id-param, user: tx-sender }))) + (user-balance (stx-get-balance tx-sender)) + ) + (asserts! (is-market-open market-id-param) ERR-MARKET-CLOSED) + (asserts! (>= amount MINIMUM-BET-AMOUNT) ERR-INVALID-BET) + (asserts! (>= user-balance amount) ERR-INSUFFICIENT-BALANCE) + + (if bet-yes + (map-set markets market-id-param + (merge market { total-yes-amount: (+ (get total-yes-amount market) amount) })) + (map-set markets market-id-param + (merge market { total-no-amount: (+ (get total-no-amount market) amount) })) + ) + + (map-set user-bets { market-id: market-id-param, user: tx-sender } + (merge user-bet { + yes-amount: (if bet-yes (+ (get yes-amount user-bet) amount) (get yes-amount user-bet)), + no-amount: (if bet-yes (get no-amount user-bet) (+ (get no-amount user-bet) amount)) + }) + ) + + (try! (transfer-token amount tx-sender (as-contract tx-sender))) + (ok true) + ) +) + +(define-public (resolve-market (market-id-param uint) (outcome bool)) + (let ((market (unwrap! (map-get? markets market-id-param) ERR-NOT-INITIALIZED))) + (asserts! (is-eq tx-sender (get creator market)) ERR-UNAUTHORIZED) + (asserts! (is-eq (get state market) "active") ERR-ALREADY-RESOLVED) + (asserts! (>= block-height (get resolution-timestamp market)) ERR-MARKET-CLOSED) + + (map-set markets market-id-param + (merge market { + outcome: (some outcome), + state: "resolved" + }) + ) + (ok true) + ) +) + +(define-public (claim-payout (market-id-param uint)) + (let ( + (market (unwrap! (map-get? markets market-id-param) ERR-NOT-INITIALIZED)) + (user-bet (unwrap! (map-get? user-bets { market-id: market-id-param, user: tx-sender }) ERR-INVALID-BET)) + (outcome (unwrap! (get outcome market) ERR-NOT-INITIALIZED)) + ) + (asserts! (is-eq (get state market) "resolved") ERR-NOT-INITIALIZED) + + (let ( + (winning-amount (if outcome (get yes-amount user-bet) (get no-amount user-bet))) + (winning-pool (if outcome (get total-yes-amount market) (get total-no-amount market))) + (losing-pool (if outcome (get total-no-amount market) (get total-yes-amount market))) + ) + (asserts! (> winning-amount u0) ERR-NO-PAYOUT) + + (let ( + (payout (calculate-payout winning-amount winning-pool losing-pool)) + ) + (map-delete user-bets { market-id: market-id-param, user: tx-sender }) + (var-set accumulated-fees (+ (var-get accumulated-fees) (- (+ winning-pool losing-pool) payout))) + (try! (as-contract (transfer-token payout tx-sender tx-sender))) + (ok payout) + ) + ) + ) +) + +(define-public (withdraw-fees) + (begin + (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-UNAUTHORIZED) + (let ((fees (var-get accumulated-fees))) + (var-set accumulated-fees u0) + (try! (as-contract (transfer-token fees tx-sender CONTRACT-OWNER))) + (ok fees) + ) + ) +) + +;; Read-only Functions +(define-read-only (get-market-info (market-id-param uint)) + (ok (unwrap! (map-get? markets market-id-param) ERR-NOT-INITIALIZED)) +) + +(define-read-only (get-user-bets (market-id-param uint) (user principal)) + (ok (default-to { yes-amount: u0, no-amount: u0 } + (map-get? user-bets { market-id: market-id-param, user: user }))) +) + +(define-read-only (get-accumulated-fees) + (ok (var-get accumulated-fees)) +) \ No newline at end of file diff --git a/predict/settings/Devnet.toml b/predict/settings/Devnet.toml new file mode 100644 index 0000000..4b94b79 --- /dev/null +++ b/predict/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/predict/tests/prediction_test.ts b/predict/tests/prediction_test.ts new file mode 100644 index 0000000..561c544 --- /dev/null +++ b/predict/tests/prediction_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); + }, +});