This is a binary LMSR (Logarithmic Market Scoring Rule) prediction market factory deployed on Stacks mainnet. It supports market creation with initial liquidity, YES/NO share trading with fees, bias/virtual liquidity for initial pricing, and admin-controlled resolution. Math operates in STX units with uSTX (1e6) for transfers.
Architecture: Single contract manages all markets via map-indexed state. A single hardcoded ADMIN address controls market lifecycle (create β bias β trade β resolve β redeem β withdraw surplus). Fees split across four recipients (drip, brc20, team, LP) with configurable BPS rates and percentage splits.
Location
buy-yes-auto, buy-no-auto β bump-cap-if-needed call
Description
The buy-yes-auto and buy-no-auto functions accept a user-supplied target-cap parameter and call bump-cap-if-needed, which unconditionally increases the user's spending cap to any value they request. This means any user can set their own cap to u340282366920938463463374607431768211455 (max uint128) and bypass the entire cap system.
The regular buy-yes / buy-no functions enforce the cap but don't allow setting it β however, a user simply calls buy-yes-auto once with a huge target-cap, then uses buy-yes freely afterward.
;; User controls target-cap β can set to any value
(define-public (buy-yes-auto (m uint) (amount uint) (target-cap uint) (max-cost uint))
(begin
...
(bump-cap-if-needed m tx-sender target-cap) ;; user sets own cap!
...
(define-private (bump-cap-if-needed (m uint) (who principal) (target-cap uint))
(let ((cur (default-to u0 (get cap (map-get? user-caps { m: m, user: who })))))
(if (> target-cap cur)
(begin (map-set user-caps { m: m, user: who } { cap: target-cap }) true)
true)))
Impact
The spending cap system is entirely cosmetic. Any user can bypass it. If the cap was intended as a risk management or regulatory control (e.g., limiting maximum exposure per user), it provides zero protection. This is particularly concerning because the task context mentions "aborted txs still set cap state" β the cap can be bumped even if the trade itself fails due to slippage or other checks, since bump-cap-if-needed is called before the trade validation.
Recommendation
Remove user-controlled cap setting from auto-buy functions. Caps should only be settable by the admin:
(define-public (set-user-cap (m uint) (who principal) (cap uint))
(begin
(try! (only-admin))
(map-set user-caps { m: m, user: who } { cap: cap })
(ok true)))
Exploit Test
;; Exploit test for C-01: Self-service cap bypass
(define-public (test-exploit-c01-cap-bypass)
(let (
;; User has no cap set (default u0)
;; Call buy-yes-auto with target-cap = max uint
;; Cap is now unlimited β user can trade without restriction
(result (buy-yes-auto u1 u1 u340282366920938463463374607431768211455 u999999999999))
)
;; Even if trade fails, cap was already bumped by bump-cap-if-needed
;; User's cap is now effectively infinite
(asserts! (> (get-cap u1 tx-sender) u0) (err u999))
(ok true)))
Location
resolve function
Description
The resolve function allows the single ADMIN to unilaterally declare any market outcome as "YES" or "NO" with no on-chain verification, oracle feed, dispute period, or community override. The admin can resolve against the actual real-world outcome to profit from positions or harm specific users.
(define-public (resolve (m uint) (result (string-ascii 3)))
(begin
(try! (only-admin)) ;; only admin, no oracle
(asserts! (is-eq (get-status-str m) "open") (err u102))
(asserts! (or (is-eq result "YES") (is-eq result "NO")) (err u103))
;; No verification of actual outcome
(map-set m-outcome { m: m } { o: result })
(map-set m-status { m: m } { s: "resolved" })
(ok true)))
Impact
Complete trust dependency on a single address. Admin can steal all pool funds by resolving in their favor. Combined with the ability to trade (admin is not excluded from trading), admin can buy shares on one side and resolve that side as winner.
Recommendation
Implement at minimum: (1) a dispute/challenge period after resolution, (2) an oracle integration for verifiable outcomes, or (3) a multisig resolution requirement. Also consider excluding the ADMIN from holding shares.
as-contract Grants Blanket Asset Access
Location
xfer-out function, all as-contract calls
Description
The contract uses as-contract (pre-Clarity 4) which gives blanket authority to transfer any asset the contract holds. With Clarity 4's as-contract? and explicit allowances (with-stx), the contract could restrict itself to only moving STX, preventing any future exploit path involving other token types.
(define-private (xfer-out (amt uint) (to principal))
(if (or (is-eq amt u0) (is-eq to (self-principal)))
(ok true)
(as-contract (stx-transfer? amt (self-principal) to))))
Impact
If any FTs or NFTs are accidentally sent to the contract, they cannot be restricted from being moved. A future upgrade or interaction could exploit this broader authority.
Recommendation
Migrate to Clarity 4 and use (as-contract? (with-stx) ...) to explicitly limit asset movement to STX only.
Location
set-fees, set-fee-recipients, set-protocol-split
Description
Until lock-fees-config is called, the admin can change fee BPS (up to 100%), fee split percentages, and all four fee recipient addresses at any time. This affects all open markets simultaneously. There is no timelock or notification mechanism. The fees-locked flag is irreversible once set, but there's no guarantee it will ever be set.
(define-public (set-fees (protocol-bps uint) (lp-bps uint))
(begin
(try! (only-admin))
(try! (guard-not-locked))
;; Can set up to 10000 bps (100%) each!
(asserts! (<= protocol-bps u10000) (err u740))
(asserts! (<= lp-bps u10000) (err u741))
...))
Impact
Admin could set protocol-fee-bps + lp-fee-bps to 20000 (200%), making every trade cost 3x the base amount. Or redirect all fee recipients to a personal wallet. Users currently trading have no protection against mid-flight fee changes.
Recommendation
Cap combined fees to a reasonable maximum (e.g., 1000 bps = 10%). Add a timelock for fee changes. Consider per-market fee snapshots at creation time.
exp-fixed Caps at 2^31-1 for Large Inputs
Location
exp-fixed function
Description
For large positive inputs (when k >= 20), exp-fixed returns a hardcoded 2147483647 (2^31-1) instead of the correct exponential value. This affects cost calculations when share quantities are large relative to the liquidity parameter b. The LMSR cost function depends on accurate exp(q/b) values.
(if (>= ku u20)
(if (< k 0) 0 (to-int u2147483647)) ;; hardcoded cap
...)
Impact
For markets where q/b > 20*ln(2) β 13.86, pricing becomes flat/inaccurate. With typical b values around initial-liquidity/ln(2), this means shares exceeding ~14x the initial liquidity (in STX) will hit the cap. Trades in this range will misprice, potentially allowing arbitrage.
Recommendation
Document the safe operating range prominently. Add a guard that rejects trades pushing q/b beyond the accurate range (e.g., q/b < 12).
ln-general Limited to 8 Normalization Steps
Location
ln-general via ln-norm-step
Description
The natural logarithm implementation uses 8 iterative halving/doubling steps to normalize the input to [0.5, 2.0] range before applying the atanh series. This limits accurate computation to inputs in approximately [2^-8, 2^8] Γ SCALE = [3906, 256000000]. Values outside this range will produce incorrect results because normalization is incomplete.
Impact
Affects cost-tilde, set-market-bias, and invert-buy-shares when intermediate exp values exceed the normalized range. Could produce incorrect pricing or bias calculations for extreme market states.
Recommendation
Increase normalization steps to 20+ to match the exp-fixed range, or add input validation to reject values outside the accurate domain.
Location
calculate-cost β do-buy conversion path
Description
The cost calculation operates in STX units (integers), and the result is converted to uSTX via stx->ustx (multiply by 1e6). However, cost-tilde returns an integer in STX, discarding sub-STX fractions. For small trades, this truncation means the actual cost in uSTX is systematically rounded down, giving buyers shares at a slight discount. The minimum cost floor is 1 STX (1,000,000 uSTX) regardless of actual LMSR price.
(baseStx (if (> c0 u0) c0 u1)) ;; floor to 1 STX minimum
(baseUstx (stx->ustx baseStx)) ;; loses sub-STX precision
Impact
For each trade, up to 999,999 uSTX (~1 STX) of cost can be lost. Over many small trades, this leaks value from the pool. The per-trade solvency check mitigates catastrophic drain but doesn't prevent gradual erosion.
Recommendation
Perform cost calculation in uSTX units natively, or use ceiling division when converting STXβuSTX for buy costs.
Location
xfer-out function
Description
xfer-out returns (ok true) without transferring if the recipient equals the contract's own principal. If admin accidentally (or intentionally) sets a fee recipient to the contract address, fees are silently kept in the contract rather than distributed. These fees then become part of the pool balance, inflating it.
(define-private (xfer-out (amt uint) (to principal))
(if (or (is-eq amt u0) (is-eq to (self-principal)))
(ok true) ;; silently no-ops
(as-contract (stx-transfer? amt (self-principal) to))))
Impact
Low β requires admin misconfiguration. Fees would be retained in the contract pool rather than lost.
Recommendation
Add a check in set-fee-recipients that rejects the contract's own address as a recipient.
Location
All public functions
Description
The contract emits no print events for any state changes. Off-chain indexers and UIs must rely on transaction inspection rather than structured event data. This makes monitoring, auditing, and dispute resolution harder.
Impact
Operational visibility only. No direct security impact, but impedes incident detection.
Recommendation
Add (print { event: "buy", market: m, side: "yes", shares: amount, cost: total, user: tx-sender }) and similar for sells, resolution, and admin actions.
Location
All admin functions
Description
All admin operations (pause, resolve, fee changes, withdrawal) take effect immediately with no delay or timelock. Users cannot react to adverse admin actions before they take effect.
Impact
Reduces user protection against admin key compromise or malicious admin behavior. Users have no time to exit positions before adverse changes.
Recommendation
Implement a timelock (e.g., 24-48 hours) for sensitive operations like resolution, fee changes, and pausing. Allow users to exit during the delay period.
Location
set-market-bias
Description
Once set-market-bias is called, m-bias-locked is set to true and can never be changed. The bias also requires zero supply (no trades yet). This is by design but means the admin must get the initial bias right on the first try.
Impact
Operational constraint. An incorrectly set bias requires creating a new market.
Location
Contract-wide
Description
Markets have no expiry block height or timestamp. An unresolved market stays open indefinitely. There is no mechanism for users to force resolution or reclaim funds from a stale market (admin could simply abandon a market).
Impact
Funds could be locked indefinitely if admin becomes unresponsive. Users can still sell shares on open markets, but at LMSR prices (not at 1:1 redemption value).
Location
do-buy β ERR-TRADE-INSOLVENT check
Description
The per-trade solvency check pool >= max(yesSupply, noSupply) * UNIT is correct for binary LMSR and is a good safety mechanism. However, due to the truncation issue in M-03, the pool can gradually erode below the theoretical LMSR solvency level between trades. The check prevents catastrophic insolvency but allows slow leakage.
Impact
Informational β the guard is sound for its purpose. The slow leak from truncation is covered in M-03.
ERR-TRADE-INSOLVENT check prevents trades that would make the pool unable to cover worst-case redemption.minu), preventing negative net proceeds.ERR-INSOLVENT-RESOLVE prevents resolving a market if the pool can't cover all winning shares.xfer-in for the total amount, avoiding reentrancy-like issues with multiple user transfers.withdraw-surplus requires market resolved and all winning shares redeemed before admin can withdraw.| Metric | Weight | Score | Weighted |
|---|---|---|---|
| Financial risk | 3 | 3 (DeFi/prediction market) | 9 |
| Deployment likelihood | 2 | 3 (deployed mainnet) | 6 |
| Code complexity | 2 | 3 (900+ lines, LMSR math) | 6 |
| User exposure | 1.5 | 2 (known project) | 3 |
| Novelty | 1.5 | 3 (LMSR with bias on Stacks) | 4.5 |
| Total | 2.85 / 3.0 | ||
Clarity version penalty: -0.5 (pre-Clarity 4) β Final: 2.35 β Above 1.8 threshold
Manual code review of the on-chain Clarity source fetched from Hiro API. All public/private functions analyzed for access control, math correctness, state consistency, and fund safety. Review focused on LMSR math accuracy, admin privilege scope, spending cap enforcement, fee mechanics, and solvency guarantees per the task specification.