sBTC Prediction Market v0 — Security Audit

kai-builder/sbtcmarket-v0 · Commit: 2f2806e (2025-10-14) · Audited: February 21, 2026

2
Critical
2
High
3
Medium
2
Low
2
Informational

Overview

sBTC Prediction Market v0 is a binary outcome prediction market built on Stacks using sBTC as collateral. It implements a complete-set AMM design: users mint YES+NO share pairs backed 1:1 by sBTC, then trade via a constant-product swap. Markets resolve against Pyth oracle price feeds within a 2-block window, or can be cancelled and refunded if the window is missed.

The contract is 1,568 lines of well-documented Clarity. The architecture is sound — complete-set issuance with proportional redemption is a proven design. However, the codebase contains a catastrophic backdoor in the form of an unrestricted mock-resolve function, several arithmetic edge cases, and insufficient protection against over-issuance impacts.

Findings Summary

IDSeverityTitle
C-01CRITICALUnrestricted mock-resolve-market allows anyone to steal all vault funds
C-02CRITICALrefund-shares underflow reverts block refunds for swap-derived positions
H-01HIGHmul-down overflow silently returns max-uint, corrupting AMM pricing
H-02HIGHNo slippage protection on swaps, buys, or sells
M-01MEDIUM2-block resolution window is dangerously narrow
M-02MEDIUMRounding truncation in proportional redemption permanently locks dust
M-03MEDIUMTax-on-sell double-charges AMM fee + tax without disclosure
L-01LOWset-tax-recipient event logs new value as old recipient
L-02LOWFee-on-swap can round to zero for small trades
I-01INFOHardcoded tax recipient address
I-02INFOOver-issuance risk is documented but has no circuit breaker

Detailed Findings

C-01 Unrestricted mock-resolve-market allows anyone to steal all vault funds

Location: sbtcmarket-v0.clar, line ~1490 (mock-resolve-market)

Description: The contract includes a testing function mock-resolve-market that resolves any open market with an arbitrary outcome. It has zero access control — any principal can call it. An attacker can:

  1. Buy YES shares on an active market
  2. Call mock-resolve-market(market-id, true) to force a YES outcome
  3. Redeem shares to drain the vault
;; NO ACCESS CONTROL — anyone can call this!
(define-public (mock-resolve-market (market-id uint) (winner bool))
  (let ((market (try! (require-market-open market-id))))
    (map-set markets { id: market-id }
      (merge market {
        resolved: true,
        outcome: (some winner),
      })
    )
    (ok winner)
  )
)

Impact: Complete theft of all vault funds across all markets. This is a total loss scenario.

Recommendation: Remove mock-resolve-market entirely before deployment. If needed for testing, gate it behind (try! (ensure (is-eq tx-sender (var-get contract-owner)) ERR-UNAUTHORIZED)).

C-02 refund-shares underflow reverts, blocking refunds for swap-derived positions

Location: sbtcmarket-v0.clar, refund-shares function

Description: When a market is cancelled, refund-shares calculates complete-sets-burned = min(yes-balance, no-balance) and subtracts it from yes-issued/no-issued. But after AMM swaps, users can hold more shares on one side than were ever issued via complete sets. Multiple users calling refund will eventually cause yes-issued to underflow past zero, reverting the transaction.

;; If total complete-sets-burned across all users > yes-issued, this underflows and REVERTS
(yes-issued: (- (get yes-issued post-burn) complete-sets-burned),
 no-issued: (- (get no-issued post-burn) complete-sets-burned),)

Impact: After sufficient swap activity, some users will be permanently unable to claim refunds on cancelled markets. Their sBTC is locked forever.

Recommendation: Cap the issued-count reduction: (min-uint complete-sets-burned (get yes-issued post-burn)).

H-01 mul-down overflow silently returns max-uint, corrupting AMM pricing

Location: mul-down helper, used by calculate-amount-out

Description: When a * b overflows uint128, mul-down returns u340282366920938463463374607431768211455 (max uint128) instead of reverting. This corrupted product is then used as k in the AMM formula, causing new-to = max-uint / new-from to return an astronomically large value.

(define-private (mul-down (a uint) (b uint))
  (let ((product (* a b)))
    (if (or (is-eq a u0) (is-eq (/ product a) b))
      product
      u340282366920938463463374607431768211455  ;; RETURNS THIS INSTEAD OF ERRORING
    )
  )
)

Impact: With large enough reserves (e.g., virtual-liquidity > ~18.4 billion), swaps will produce wildly incorrect outputs. Could be exploited by creating a market with very high virtual-liquidity.

Recommendation: Return (err ERR-INVALID-V-LIQUIDITY) on overflow instead of a sentinel value.

H-02 No slippage protection on swaps, buys, or sells

Location: swap-shares, buy-shares, sell-shares

Description: None of the trading functions accept a min-amount-out parameter. Users submit transactions that may sit in the mempool while other trades move the price. By the time their transaction executes, they may receive significantly fewer shares than expected.

Impact: Users are vulnerable to sandwich attacks and unfavorable price movements.

Recommendation: Add min-amount-out parameter to all trading functions. Revert if output is below the minimum.

M-01 2-block resolution window is dangerously narrow

Location: resolve-market, CANCEL-TIMEOUT-BLOCKS = u2

Description: Markets can only be resolved during a 2 Bitcoin-block window (resolution-block to resolution-block + 1). Bitcoin blocks average 10 minutes but can take over an hour. If the resolver misses this window, the market cannot be resolved and must be cancelled.

Impact: High probability of markets failing to resolve, especially if oracle data isn't available at exactly the right block.

Recommendation: Increase resolution window to 10+ blocks and increase cancel timeout to 50+ blocks.

M-02 Rounding truncation in proportional redemption permanently locks dust

Location: redeem-shares, refund-shares

Description: The formula payout = (user-shares * vault-balance) / total-circulating truncates (rounds down). After all users redeem, residual sats remain in the vault with no mechanism to recover them.

Impact: Small amounts of sBTC permanently locked per market. Individually minor but accumulates.

Recommendation: Allow the last redeemer to claim remaining vault balance, or add an admin sweep function.

M-03 Tax-on-sell double-charges AMM fee + tax without disclosure

Location: sell-shares

Description: sell-shares first calls swap-shares (which charges the AMM fee), then deducts a 1% tax on the burn amount. Users pay both the swap fee AND the tax. Effective cost is ~1.3% but this isn't transparent.

Impact: Users pay more in fees than they expect.

Recommendation: Document the total fee structure clearly. Consider applying tax only once per direction.

L-01 set-tax-recipient event logs new value as old recipient

Location: set-tax-recipient

(var-set tax-recipient new-recipient)  ;; Sets FIRST
(print {
  old-recipient: (var-get tax-recipient),  ;; Reads NEW value!
  new-recipient: new-recipient,
})

Impact: Misleading event data for off-chain indexers.

Recommendation: Cache the old value in a let before calling var-set.

L-02 Fee-on-swap can round to zero for small trades

Location: swap-shares

Description: fee-share = (amount-in * fee-bps) / FEE-DENOMINATOR. For trades where amount-in * fee-bps < 10000, the fee rounds to zero. Small trades pay no fee.

Impact: Fee leakage. Could be exploited by splitting large trades into many small ones.

Recommendation: Set a minimum fee of 1 sat, or round up.

I-01 Hardcoded tax recipient address

Location: (define-data-var tax-recipient principal 'ST5X3MK1FVHW52WRZN720041Y138263QSS9NR9AE)

Description: Initial tax recipient is hardcoded to a testnet address. Will collect tax if not changed after deployment.

Recommendation: Set initial tax-recipient to tx-sender (deployer).

I-02 Over-issuance risk is documented but has no circuit breaker

Description: The contract documents that swaps can create net new shares beyond vault collateral. While proportional redemption handles this fairly, there is no maximum over-issuance ratio or warning mechanism.

Recommendation: Add a get-collateralization-ratio read-only function. Consider a max over-issuance threshold that pauses swaps.

Architecture Assessment

The core design — complete-set AMM with proportional redemption — is well-architected. The developer clearly understands the over-issuance tradeoff and documents it thoroughly. Code quality is above average for Clarity projects.

The critical issues are both fixable: remove the mock function and cap the issued-count subtraction. With those fixes plus slippage protection, this would be a solid prediction market contract.

Positive Observations