phessophissy/btc-prediction-market ·
Commit: c94ab89 (2026-02-05) ·
Audited: February 21, 2026
BTC Prediction Market is a decentralized prediction market built on Stacks that uses Bitcoin block hashes for provably fair settlement. Users create binary or multi-outcome markets, bet STX on outcomes, and the winning outcome is determined by the Bitcoin block hash at a predetermined height. The project includes three contract versions (v1, v2, v3), a SIP-010 reward token (PMT), a frontend, SDK, and CLI tools.
The codebase spans 5 Clarity contracts totaling ~1,464 lines. The contracts claim Clarity 4 features (bitwise operations, tenure-height, get-burn-block-info?) but still use the legacy as-contract instead of Clarity 4's as-contract?.
| Metric | Weight | Score | Weighted |
|---|---|---|---|
| Financial risk | 3 | 3 — DeFi prediction market, holds/transfers STX bet pools | 9 |
| Deployment likelihood | 2 | 2 — Has frontend, SDK, CLI, mainnet deployment plan | 4 |
| Code complexity | 2 | 3 — 5 contracts, 1464 lines, multi-version architecture | 6 |
| User exposure | 1.5 | 1 — 3 stars, 2 forks | 1.5 |
| Novelty | 1.5 | 3 — Bitcoin-hash-based prediction market, unique settlement | 4.5 |
| Total | 25 / 10 = 2.5 ≥ 1.8 ✓ | ||
| ID | Severity | Title |
|---|---|---|
| C-01 | CRITICAL | claim-winnings sends payout to contract, not user |
| C-02 | CRITICAL | withdraw-fees drains user bet deposits |
| H-01 | HIGH | Emergency withdrawal enables rug pull of all user funds |
| H-02 | HIGH | V2 and V3 are incomplete — missing bet/settle/claim functions |
| M-01 | MEDIUM | V1 ignores platform-paused flag on betting and market creation |
| M-02 | MEDIUM | Integer division truncation leaks dust in payout calculation |
| M-03 | MEDIUM | V1 creation fee bypasses contract — sent directly to deployer |
| L-01 | LOW | Participant list silently drops users after 500 |
| L-02 | LOW | No market cancellation — unsettled markets lock funds forever |
| I-01 | INFO | Uses legacy as-contract instead of Clarity 4 as-contract? |
| I-02 | INFO | Prediction token (PMT) not integrated with market contracts |
Location: btc-prediction-market.clar — claim-winnings function
Description: The payout transfer uses an obfuscated pattern to resolve the recipient. Inside as-contract, tx-sender resolves to the contract principal itself, not the calling user. The code constructs (list tx-sender) then indexes into it — this evaluates to the contract's own address. The STX is transferred from the contract back to the contract, meaning winners can never actually receive their funds.
;; Inside as-contract block, tx-sender = contract address
(try! (as-contract (stx-transfer? net-payout tx-sender
(unwrap! (element-at? (list tx-sender) u0) ERR-TRANSFER-FAILED))))
;; ^^^^^^^^^^ = contract address, NOT user
Impact: All winning payouts are permanently locked in the contract. No user can ever claim their winnings. The total-pool STX accumulates with no way to extract it (except via the owner's withdraw-fees — see C-02).
Recommendation: Capture the caller's address before entering as-contract:
(let ((claimer tx-sender))
...
(try! (as-contract (stx-transfer? net-payout tx-sender claimer))))
Location: btc-prediction-market.clar, btc-prediction-market-v2.clar, btc-prediction-market-v3.clar — withdraw-fees function
Description: The withdraw-fees function allows the contract owner to transfer any amount of STX from the contract to any recipient. There is no check that the amount is limited to collected fees — the owner can withdraw the entire contract balance, including all user bet deposits from active, unsettled markets.
(define-public (withdraw-fees (amount uint) (recipient principal))
(begin
(asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
;; No check: amount <= actual fees. Can drain everything.
(as-contract (stx-transfer? amount tx-sender recipient))))
Impact: Contract owner can steal all user deposits at any time. Combined with C-01 (payouts going back to contract), the owner is the only entity that can ever extract funds.
Recommendation: Track fee revenue separately and enforce (asserts! (<= amount (var-get withdrawable-fees)) ERR-INSUFFICIENT-FUNDS). Decrement the tracked fees after withdrawal.
Location: btc-prediction-market-v2.clar, btc-prediction-market-v3.clar — emergency-withdraw-all, emergency-withdraw
Description: The owner can unilaterally enable emergency mode and immediately drain all contract funds. There is no timelock, no multi-sig requirement, and no user notification period. Emergency mode is a single boolean flip that enables unrestricted withdrawals.
;; Step 1: Owner flips emergency mode on
(define-public (enable-emergency-mode) ...)
;; Step 2: Owner drains everything (same transaction possible)
(define-public (emergency-withdraw-all)
(let ((contract-balance (stx-get-balance (as-contract tx-sender))))
(asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-AUTHORIZED)
(asserts! (var-get emergency-mode) ERR-NOT-AUTHORIZED)
(try! (as-contract (stx-transfer? contract-balance tx-sender (var-get contract-owner))))
...))
Impact: Owner can rug-pull all user funds instantly with no warning or recourse.
Recommendation: Add a timelock (e.g., 144 Bitcoin blocks ≈ 1 day) between enabling emergency mode and withdrawing. Allow users to claim their positions during the grace period. Consider requiring a multi-sig or DAO vote.
Location: btc-prediction-market-v2.clar, btc-prediction-market-v3.clar
Description: V2 and V3 only implement create-binary-market, ownership transfer, and emergency functions. They are entirely missing: place-bet-internal (and all bet-outcome-* functions), settle-market, and claim-winnings. Markets can be created but nobody can bet, settle, or claim. The mainnet deployment plan (deployments/default.mainnet-plan.yaml) deploys all three versions.
Impact: If V2 or V3 are deployed and used, user creation fees (0.1 STX) are collected but markets are non-functional. Funds paid as creation fees are unrecoverable by the creator since markets can never complete.
Recommendation: Either complete V2/V3 with all necessary functions, or remove them from the deployment plan. Do not deploy incomplete contracts that accept user funds.
Location: btc-prediction-market.clar — create-binary-market, create-multi-market, place-bet-internal
Description: V1 defines platform-paused and set-platform-paused but never checks the flag in any market creation or betting function. The admin pause mechanism is dead code — setting it to true has no effect on user operations. (V2 and V3 do check it in create-binary-market.)
Impact: Admin cannot halt operations during an incident. If a bug is discovered, there's no way to stop users from continuing to bet.
Recommendation: Add (asserts! (not (var-get platform-paused)) ERR-NOT-AUTHORIZED) to all state-changing functions in V1.
Location: btc-prediction-market.clar — claim-winnings
Description: The payout formula (/ (* user-winning-amount total-pool) winning-pool) performs integer division, truncating any remainder. Over many claims, the accumulated rounding error ("dust") remains locked in the contract with no mechanism to recover it.
(gross-payout (if (is-eq winning-pool u0)
u0
(/ (* user-winning-amount total-pool) winning-pool)))
Impact: Small amounts of STX are permanently locked after every market settlement. While individually insignificant, this accumulates over time. In extreme cases (very small bets relative to pool), users may receive 0 payout due to truncation.
Recommendation: Use a higher-precision intermediate calculation (multiply by 1e6 first, then divide), or implement a "last claimer gets remainder" pattern.
Location: btc-prediction-market.clar — create-binary-market, create-multi-market
Description: Market creation fees are transferred directly to CONTRACT-OWNER (the deployer's address), not to the contract. The total-fees-collected variable is incremented but tracks phantom revenue — the contract never held these fees. This means withdraw-fees would need to draw from user deposits to honor the tracked amount.
(try! (stx-transfer? MARKET-CREATION-FEE tx-sender CONTRACT-OWNER))
;; Fee goes directly to deployer, never touches the contract
(var-set total-fees-collected (+ (var-get total-fees-collected) MARKET-CREATION-FEE))
;; But this counter claims the contract collected it
Impact: Accounting mismatch. get-total-fees-collected reports a higher number than the contract actually holds, misleading integrators and users.
Recommendation: Either send creation fees to the contract (via as-contract tx-sender) and track them properly, or remove the fee counter when fees go directly to the owner.
Location: btc-prediction-market.clar — add-participant
Description: The market-participants map stores a (list 500 principal). The add-participant function uses as-max-len? which returns none when the list is full, causing the unwrap! to return false — silently failing. Users who bet after the 500th unique participant are not recorded in the participants list.
(map-set market-participants market-id
(unwrap! (as-max-len? (append current-participants user) u500) false))
;; ^^^^^ silent failure
Impact: Late participants are invisible in the participant list. If any off-chain system uses this list for airdrops, notifications, or analytics, it will miss users.
Recommendation: Return an error or use a separate counter-based map for participant tracking that doesn't have a fixed size limit.
Location: All market contracts
Description: There is no function to cancel a market or allow users to withdraw their bets from an unsettled market. If get-burn-block-info? returns none for the settlement height (e.g., the block hash is pruned or unavailable), the market can never be settled and all deposited funds are permanently locked.
Impact: Users lose their deposits if settlement becomes impossible. There is no admin or user-initiated refund mechanism.
Recommendation: Add a cancel-market function (admin-only or after a timeout) that refunds all bettors proportionally. Add a withdraw-bet function for markets that pass a deadline without settlement.
Location: All contracts
Description: Despite claiming to be "Built with Clarity 4 features" and using Clarity 4 builtins like tenure-height and bitwise operations, all contracts use the legacy as-contract instead of the safer as-contract? with explicit asset allowances introduced in Clarity 4.
Impact: Missed opportunity for defense-in-depth. as-contract? with with-stx allowances would limit the blast radius of bugs like C-01 and C-02.
Recommendation: Migrate to (as-contract? (with-stx amount) ...) for all STX transfers. This explicitly declares which assets the contract context can move.
Location: prediction-token.clar
Description: The PMT reward token is fully implemented with mint/burn/batch-reward functions, but no market contract ever calls it. The use-trait sip-010-trait import in v1 is unused. The token exists in isolation with no connection to the prediction market ecosystem.
Impact: Dead code. The token serves no purpose in the current system.
Recommendation: Either integrate PMT rewards into market participation (e.g., mint tokens on bet placement or correct predictions), or remove the token contract to reduce attack surface.