Source Code

;; Prediction Pools - Multi-Pool Prediction Market on Stacks
;; Anyone can create prediction pools with custom outcomes
;; Supports STX and USDCx payments
;; Built for Stacks Prediction Market

;; ============================================
;; CONSTANTS
;; ============================================

(define-constant CONTRACT-OWNER tx-sender)

;; Token types (u0 = STX, u1 = USDCx)
(define-constant TOKEN-STX u0)
(define-constant TOKEN-USDCX u1)

;; Error codes
(define-constant ERR-UNAUTHORIZED (err u401))
(define-constant ERR-INVALID-AMOUNT (err u400))
(define-constant ERR-POOL-NOT-FOUND (err u404))
(define-constant ERR-POOL-SETTLED (err u409))
(define-constant ERR-INVALID-OUTCOME (err u422))
(define-constant ERR-NOT-SETTLED (err u412))
(define-constant ERR-ALREADY-CLAIMED (err u410))
(define-constant ERR-NO-WINNINGS (err u411))
(define-constant ERR-POOL-EXPIRED (err u413))
(define-constant ERR-POOL-NOT-EXPIRED (err u414))
(define-constant ERR-INVALID-TITLE (err u420))
(define-constant ERR-INVALID-DESCRIPTION (err u421))
(define-constant ERR-INVALID-DURATION (err u423))
(define-constant ERR-ALREADY-BET (err u424))
(define-constant ERR-POOL-ACTIVE (err u425))
(define-constant ERR-DEPOSIT-NOT-CLAIMED (err u426))
(define-constant ERR-DEPOSIT-ALREADY-CLAIMED (err u427))
(define-constant ERR-INVALID-TOKEN-TYPE (err u428))
(define-constant ERR-TOKEN-TRANSFER-FAILED (err u429))

;; Configuration constants
(define-constant FEE-PERCENT u2) ;; 2% platform fee
(define-constant MIN-BET-AMOUNT-STX u1000000) ;; 1 STX minimum bet (6 decimals)
(define-constant MIN-BET-AMOUNT-USDCX u1000000) ;; 1 USDC minimum bet (6 decimals)
(define-constant POOL-DEPOSIT-STX u5000000) ;; 5 STX deposit to create pool
(define-constant POOL-DEPOSIT-USDCX u5000000) ;; 5 USDC deposit to create pool
(define-constant MIN-DURATION u144) ;; ~24 hours minimum (1 block per 10 min)
(define-constant MAX-DURATION u4320) ;; ~30 days maximum

;; ============================================
;; DATA STRUCTURES
;; ============================================

;; Pool data structure
(define-map pools
    { pool-id: uint }
    {
        creator: principal,
        title: (string-ascii 128),
        description: (string-ascii 256),
        outcome-a: (string-ascii 64),
        outcome-b: (string-ascii 64),
        category: (string-ascii 32),
        token-type: uint,  ;; u0 = STX, u1 = USDCx
        total-a: uint,
        total-b: uint,
        settled: bool,
        winning-outcome: (optional uint),
        created-at: uint,
        expiry: uint,
        deposit-claimed: bool
    }
)

;; User bets per pool
(define-map user-bets
    { pool-id: uint, user: principal }
    {
        amount-a: uint,
        amount-b: uint
    }
)

;; Claims tracking (winners who claimed)
(define-map claims
    { pool-id: uint, user: principal }
    bool
)

;; Refund tracking (for expired unsettled pools)
(define-map refunds
    { pool-id: uint, user: principal }
    bool
)

;; Global counters
(define-data-var pool-counter uint u0)
(define-data-var total-volume uint u0)
(define-data-var total-fees-collected uint u0)

;; ============================================
;; READ-ONLY FUNCTIONS
;; ============================================

(define-read-only (get-pool-count)
    (var-get pool-counter)
)

(define-read-only (get-pool (pool-id uint))
    (map-get? pools { pool-id: pool-id })
)

(define-read-only (get-user-bet (pool-id uint) (user principal))
    (map-get? user-bets { pool-id: pool-id, user: user })
)

(define-read-only (has-claimed (pool-id uint) (user principal))
    (default-to false (map-get? claims { pool-id: pool-id, user: user }))
)

(define-read-only (has-refunded (pool-id uint) (user principal))
    (default-to false (map-get? refunds { pool-id: pool-id, user: user }))
)

(define-read-only (get-pool-totals (pool-id uint))
    (match (map-get? pools { pool-id: pool-id })
        pool { total-a: (get total-a pool), total-b: (get total-b pool) }
        { total-a: u0, total-b: u0 }
    )
)

(define-read-only (get-total-volume)
    (var-get total-volume)
)

(define-read-only (get-min-bet-amount (token-type uint))
    (if (is-eq token-type TOKEN-STX)
        MIN-BET-AMOUNT-STX
        MIN-BET-AMOUNT-USDCX
    )
)

(define-read-only (get-pool-deposit (token-type uint))
    (if (is-eq token-type TOKEN-STX)
        POOL-DEPOSIT-STX
        POOL-DEPOSIT-USDCX
    )
)

;; ============================================
;; PRIVATE HELPER FUNCTIONS
;; ============================================

;; Transfer tokens from user to contract
(define-private (transfer-to-contract (token-type uint) (amount uint) (sender principal))
    (if (is-eq token-type TOKEN-STX)
        (stx-transfer? amount sender current-contract)
        (contract-call? 'SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE.usdcx transfer amount sender current-contract none)
    )
)

;; Transfer tokens from contract to user  
(define-private (transfer-from-contract (token-type uint) (amount uint) (recipient principal))
    (if (is-eq token-type TOKEN-STX)
        (as-contract? ((with-stx amount)) (unwrap-panic (stx-transfer? amount tx-sender recipient)))
        (as-contract? ((with-ft 'SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE.usdcx "usdcx" amount)) (unwrap-panic (contract-call? 'SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE.usdcx transfer amount tx-sender recipient none)))
    )
)

;; ============================================
;; PUBLIC FUNCTIONS
;; ============================================

;; Create a new prediction pool
;; token-type: u0 = STX, u1 = USDCx
;; Requires deposit that is refundable after settlement
(define-public (create-pool 
    (title (string-ascii 128)) 
    (description (string-ascii 256)) 
    (outcome-a (string-ascii 64)) 
    (outcome-b (string-ascii 64))
    (category (string-ascii 32))
    (duration uint)
    (token-type uint))
    (let 
        (
            (pool-id (var-get pool-counter))
            (creator tx-sender)
            (deposit-amount (get-pool-deposit token-type))
        )
        ;; Validate inputs
        (asserts! (> (len title) u0) ERR-INVALID-TITLE)
        (asserts! (> (len outcome-a) u0) ERR-INVALID-OUTCOME)
        (asserts! (> (len outcome-b) u0) ERR-INVALID-OUTCOME)
        (asserts! (>= duration MIN-DURATION) ERR-INVALID-DURATION)
        (asserts! (<= duration MAX-DURATION) ERR-INVALID-DURATION)
        (asserts! (or (is-eq token-type TOKEN-STX) (is-eq token-type TOKEN-USDCX)) ERR-INVALID-TOKEN-TYPE)

        ;; Transfer deposit from creator to contract
        (try! (transfer-to-contract token-type deposit-amount creator))

        ;; Create pool
        (map-set pools
            { pool-id: pool-id }
            {
                creator: creator,
                title: title,
                description: description,
                outcome-a: outcome-a,
                outcome-b: outcome-b,
                category: category,
                token-type: token-type,
                total-a: u0,
                total-b: u0,
                settled: false,
                winning-outcome: none,
                created-at: stacks-block-height,
                expiry: (+ stacks-block-height duration),
                deposit-claimed: false
            }
        )

        ;; Increment counter
        (var-set pool-counter (+ pool-id u1))

        ;; Emit event
        (print {
            event: "pool-created",
            pool-id: pool-id,
            creator: creator,
            title: title,
            outcome-a: outcome-a,
            outcome-b: outcome-b,
            category: category,
            token-type: token-type,
            expiry: (+ stacks-block-height duration)
        })

        (ok pool-id)
    )
)

;; Place a bet on a pool
;; outcome: 0 = outcome-a, 1 = outcome-b
;; Must use the same token type as the pool
(define-public (place-bet (pool-id uint) (outcome uint) (amount uint))
    (let 
        (
            (pool (unwrap! (map-get? pools { pool-id: pool-id }) ERR-POOL-NOT-FOUND))
            (user tx-sender)
            (token-type (get token-type pool))
            (min-bet (get-min-bet-amount token-type))
            (existing-bet (default-to { amount-a: u0, amount-b: u0 } 
                (map-get? user-bets { pool-id: pool-id, user: user })))
        )
        ;; Validations
        (asserts! (not (get settled pool)) ERR-POOL-SETTLED)
        (asserts! (< stacks-block-height (get expiry pool)) ERR-POOL-EXPIRED)
        (asserts! (or (is-eq outcome u0) (is-eq outcome u1)) ERR-INVALID-OUTCOME)
        (asserts! (>= amount min-bet) ERR-INVALID-AMOUNT)

        ;; Transfer tokens from user to contract
        (try! (transfer-to-contract token-type amount user))

        ;; Update user bet
        (if (is-eq outcome u0)
            (map-set user-bets
                { pool-id: pool-id, user: user }
                { 
                    amount-a: (+ (get amount-a existing-bet) amount), 
                    amount-b: (get amount-b existing-bet) 
                }
            )
            (map-set user-bets
                { pool-id: pool-id, user: user }
                { 
                    amount-a: (get amount-a existing-bet), 
                    amount-b: (+ (get amount-b existing-bet) amount) 
                }
            )
        )

        ;; Update pool totals
        (if (is-eq outcome u0)
            (map-set pools
                { pool-id: pool-id }
                (merge pool { total-a: (+ (get total-a pool) amount) })
            )
            (map-set pools
                { pool-id: pool-id }
                (merge pool { total-b: (+ (get total-b pool) amount) })
            )
        )

        ;; Update total volume
        (var-set total-volume (+ (var-get total-volume) amount))

        ;; Emit event
        (print {
            event: "bet-placed",
            pool-id: pool-id,
            user: user,
            outcome: outcome,
            amount: amount,
            token-type: token-type
        })

        (ok true)
    )
)

;; Settle a pool (only creator can settle)
;; winning-outcome: 0 = outcome-a wins, 1 = outcome-b wins
(define-public (settle-pool (pool-id uint) (winning-outcome uint))
    (let 
        (
            (pool (unwrap! (map-get? pools { pool-id: pool-id }) ERR-POOL-NOT-FOUND))
            (token-type (get token-type pool))
            (total-pool (+ (get total-a pool) (get total-b pool)))
            (fee (/ (* total-pool FEE-PERCENT) u100))
        )
        ;; Only creator can settle
        (asserts! (is-eq tx-sender (get creator pool)) ERR-UNAUTHORIZED)
        (asserts! (not (get settled pool)) ERR-POOL-SETTLED)
        (asserts! (or (is-eq winning-outcome u0) (is-eq winning-outcome u1)) ERR-INVALID-OUTCOME)

        ;; Transfer fee to contract owner
        (if (> fee u0)
            (try! (transfer-from-contract token-type fee CONTRACT-OWNER))
            true
        )

        ;; Update fees collected
        (var-set total-fees-collected (+ (var-get total-fees-collected) fee))

        ;; Mark pool as settled
        (map-set pools
            { pool-id: pool-id }
            (merge pool { 
                settled: true, 
                winning-outcome: (some winning-outcome) 
            })
        )

        ;; Emit event
        (print {
            event: "pool-settled",
            pool-id: pool-id,
            winning-outcome: winning-outcome,
            total-pool: total-pool,
            fee: fee,
            token-type: token-type
        })

        (ok true)
    )
)

;; Claim winnings from a settled pool
(define-public (claim-winnings (pool-id uint))
    (let 
        (
            (claimer tx-sender)
            (pool (unwrap! (map-get? pools { pool-id: pool-id }) ERR-POOL-NOT-FOUND))
            (token-type (get token-type pool))
            (user-bet (unwrap! (map-get? user-bets { pool-id: pool-id, user: claimer }) ERR-NO-WINNINGS))
            (winning-outcome (unwrap! (get winning-outcome pool) ERR-NOT-SETTLED))
            (total-pool (+ (get total-a pool) (get total-b pool)))
            (fee (/ (* total-pool FEE-PERCENT) u100))
            (net-pool (- total-pool fee))
            (winning-pool (if (is-eq winning-outcome u0) (get total-a pool) (get total-b pool)))
            (user-winning-bet (if (is-eq winning-outcome u0) (get amount-a user-bet) (get amount-b user-bet)))
        )
        ;; Validations
        (asserts! (get settled pool) ERR-NOT-SETTLED)
        (asserts! (not (has-claimed pool-id claimer)) ERR-ALREADY-CLAIMED)
        (asserts! (> user-winning-bet u0) ERR-NO-WINNINGS)
        (asserts! (> winning-pool u0) ERR-NO-WINNINGS)

        ;; Calculate share: (user_bet * net_pool) / winning_pool
        (let ((share (/ (* user-winning-bet net-pool) winning-pool)))
            ;; Transfer winnings to user
            (try! (transfer-from-contract token-type share claimer))

            ;; Mark as claimed
            (map-set claims { pool-id: pool-id, user: claimer } true)

            ;; Emit event
            (print {
                event: "winnings-claimed",
                pool-id: pool-id,
                user: claimer,
                amount: share,
                token-type: token-type
            })

            (ok share)
        )
    )
)

;; Claim deposit after pool is settled (for pool creators)
(define-public (claim-deposit (pool-id uint))
    (let 
        (
            (pool (unwrap! (map-get? pools { pool-id: pool-id }) ERR-POOL-NOT-FOUND))
            (token-type (get token-type pool))
            (deposit-amount (get-pool-deposit token-type))
            (creator (get creator pool))
        )
        ;; Only creator can claim deposit
        (asserts! (is-eq tx-sender creator) ERR-UNAUTHORIZED)
        ;; Pool must be settled
        (asserts! (get settled pool) ERR-NOT-SETTLED)
        ;; Deposit not already claimed
        (asserts! (not (get deposit-claimed pool)) ERR-DEPOSIT-ALREADY-CLAIMED)

        ;; Transfer deposit back to creator
        (try! (transfer-from-contract token-type deposit-amount creator))

        ;; Mark deposit as claimed
        (map-set pools
            { pool-id: pool-id }
            (merge pool { deposit-claimed: true })
        )

        ;; Emit event
        (print {
            event: "deposit-claimed",
            pool-id: pool-id,
            creator: creator,
            amount: deposit-amount,
            token-type: token-type
        })

        (ok deposit-amount)
    )
)

;; Request refund for expired unsettled pool
(define-public (request-refund (pool-id uint))
    (let 
        (
            (claimer tx-sender)
            (pool (unwrap! (map-get? pools { pool-id: pool-id }) ERR-POOL-NOT-FOUND))
            (token-type (get token-type pool))
            (user-bet (unwrap! (map-get? user-bets { pool-id: pool-id, user: claimer }) ERR-NO-WINNINGS))
            (refund-amount (+ (get amount-a user-bet) (get amount-b user-bet)))
        )
        ;; Pool must be expired
        (asserts! (> stacks-block-height (get expiry pool)) ERR-POOL-NOT-EXPIRED)
        ;; Pool must not be settled
        (asserts! (not (get settled pool)) ERR-POOL-SETTLED)
        ;; Not already refunded
        (asserts! (not (has-refunded pool-id claimer)) ERR-ALREADY-CLAIMED)
        ;; Must have bet
        (asserts! (> refund-amount u0) ERR-NO-WINNINGS)

        ;; Transfer refund to user
        (try! (transfer-from-contract token-type refund-amount claimer))

        ;; Mark as refunded
        (map-set refunds { pool-id: pool-id, user: claimer } true)

        ;; Emit event
        (print {
            event: "refund-claimed",
            pool-id: pool-id,
            user: claimer,
            amount: refund-amount,
            token-type: token-type
        })

        (ok refund-amount)
    )
)

;; Note: Deposit is NOT returned for abandoned pools - this incentivizes proper settlement

Functions (17)

FunctionAccessArgs
get-pool-countread-only
get-poolread-onlypool-id: uint
get-user-betread-onlypool-id: uint, user: principal
has-claimedread-onlypool-id: uint, user: principal
has-refundedread-onlypool-id: uint, user: principal
get-pool-totalsread-onlypool-id: uint
get-total-volumeread-only
get-min-bet-amountread-onlytoken-type: uint
get-pool-depositread-onlytoken-type: uint
transfer-to-contractprivatetoken-type: uint, amount: uint, sender: principal
transfer-from-contractprivatetoken-type: uint, amount: uint, recipient: principal
create-poolpublictitle: (string-ascii 128
place-betpublicpool-id: uint, outcome: uint, amount: uint
settle-poolpublicpool-id: uint, winning-outcome: uint
claim-winningspublicpool-id: uint
claim-depositpublicpool-id: uint
request-refundpublicpool-id: uint