This doc captures a non-obvious detail of the 1v1 vault flow that made the client-side prediction lie by 10–15% until we fixed it. Keep it in mind when reusing this codebase for any flow that has vault commits + AMM trading + pro-rata redemption.
-
Commit phase (
commit.rs) — creator deposits $A on YES, challenger deposits $B on NO. The collateral lands in the vault account, both committers' positions are recorded invault.yes_total/vault.no_total. No outcome tokens are minted yet. -
Launch (
launch_vault_market.rs) — vault collateral ($A + $B) is moved into the pool'scollateral_reserve. The pool is initialized atP_initial = A / (A+B)withL = A + B(= the actual collateral). Reserves(x*, y*) = compute_reserves(P_initial, L)are minted intopool.yes_reserveandpool.no_reserve.**Then a separate batch of YES and NO is minted to
vault_yesandvault_nofor committers to claim later — both sides receive `total = A- B` extra tokens, on top of the AMM's own reserves.** This is the detail that breaks naive payout predictions:
yes_mint.supply = x* + totalno_mint.supply = y* + totalcollateral = total
-
Audience bet (
buy_outcome_tokens+swap) — user puts $X in, receives(X − fee) × 0.99²of YES + NO tokens (1% off-chain platform fee, 1% on-chain LP fee), then swaps the unwanted side into the pool for extra tokens of the chosen side. The swap moves price along the Gaussian curve; total user holding =mint + swap_bonus. -
Resolution (
redeem.rs:144-154) — for the winning side:circulating = winning_mint.supply − winning_pool_reserve if circulating == 0 || collateral >= circulating: payout_per_token = $1 # solvent else: payout_per_token = collateral / circulating # pro-rataIn the 1v1 flow the launch step inflates
winning_mint.supplybytotal(the committer claim mint) without adding any new collateral, socirculating > collateralis the default path — every winning token redeems for less than $1.
- Mathis commits $10 YES, MathisNo commits $10 NO →
total = $20 - Launch:
L = $20,(x*, y*) ≈ ($7.98, $7.98). Vault gets 20 YES + 20 NO claimable;yes_mint.supply = $27.98, collateral = $20. - Tanf bets $5 YES (1% platform fee + 1% LP fee). She mints $4.9005 of each side, swaps $4.9005 NO → $3.51 YES through the pool. Final Tanf holdings: 8.41 YES.
- After Tanf's bet:
yes_mint.supply ≈ 32.88,pool.yes_reserve ≈ 4.46, circulating ≈ 28.42- collateral ≈ $24.95
- circulating > collateral → pro-rata
- payout/token = 24.95 / 28.42 ≈ $0.878
- Tanf's actual cash-out: 8.41 × 0.878 = $7.39 ✓
The naive UI showed "If YES wins: $8.41" — overpromising by $1.02 (13.7%).
payoutPerToken(mintSupply, poolReserve, collateral)— pure helper that mirrorsredeem.rs:144-154.simulateBuyOutcomereturnsyesReserveAfter/noReserveAfterso the caller can projectpool.yes_reservepost-swap.- The bet page now fetches
getMint(yes_mint)andgetMint(no_mint)alongside the pool state, projects the post-bet supply (+= mintTokens), then runspayoutPerTokenfor both winning scenarios. - The "current position" P&L block (shown when no amount is typed)
also applies
payoutPerTokento existing user holdings, so an OG committer staring at the leaderboard sees their realistic redemption, not the inflated 1:1 number.
Any new product built on the same on-chain primitives should adopt the
same client-side accounting. The pure-AMM Viber flow (commits removed —
Mathis bootstraps the pool via add_liquidity) does not add to
mint.supply, so circulating == pool_reserve at launch and the pro-rata
clamp never kicks in unless very large audience trades drain the pool. We
keep payoutPerToken because it's still the correct formula; it just
becomes a no-op in the typical case there.