On-Chain Programs
Three Anchor programs:
| Program | ID | Role |
|---|---|---|
high-market-program | 4Pe4r9Fpa… | The LMSR AMM — live on mainnet |
markets | GoFTc9SFq… | CLOB market lifecycle and settlement |
high-market-parlay | 3iZaRuW8D… | GoCombo multi-leg slips |
→ Two Venues · Contracts & Audits
high-market-program (AMM)
Section titled “high-market-program (AMM)”The venue in production. Pricing is fully on-chain — there is no matching layer and no off-chain service in the trade path.
Instructions
Section titled “Instructions”| Instruction | Who | Purpose |
|---|---|---|
initialize | Deployer | Config PDA |
create_market | Admin (multisig) | Market PDA + YES/NO mints + USDC vault |
buy_shares | Anyone | Buy YES or NO against the curve |
sell_shares | Anyone | Sell back to the curve |
claim_winnings | Holder | Winning shares → USDC after resolution |
| lifecycle | Admin / anyone | Close, resolve, dispute, finalize, cancel, propagate event resolution |
| Account | Seeds |
|---|---|
| Config | ["config"] |
| Market | ["market", market_id] |
| YES / NO mint | ["yes_mint" | "no_mint", market_key] |
| Vault | ["vault", market_key] |
| Event / MarketEventLink | per event, per member |
C(q_yes, q_no) = b · ln( e^(q_yes/b) + e^(q_no/b) )
price_yes = 1 / (1 + e^((q_no − q_yes)/b))
buy cost = C(q+shares, …) − C(q, …)sell payout = C(q, …) − C(q−shares, …)Computed with the log-sum-exp trick so exp() only ever sees non-positive
values. Prices always sum to 1, and sit at 0.50 when q_yes == q_no.
All arithmetic is 6-decimal fixed point: 1 USDC = 1 share unit = 1_000_000.
Events group markets
Section titled “Events group markets”2–32 markets with mutually exclusive outcomes can be grouped into an Event. No USDC sits on the event PDA — per-market LMSR, disputes and vaults are unchanged. The event only coordinates resolution: an admin names the winning member, then permissionless “propagate” calls push the YES/NO outcome onto each member’s normal dispute cycle.
A MarketEventLink PDA per member is also what the parlay’s
correlation guard reads.
TWAP accumulator
Section titled “TWAP accumulator”buy_shares and sell_shares both call accrue_twap, maintaining
price_cumulative_yes plus rolling anchors over a one-hour window. This is
the reference implementation the CLOB’s accumulator was ported from, and what
parlay legs price against.
markets (CLOB)
Section titled “markets (CLOB)”Instructions
Section titled “Instructions”| Instruction | Who | Purpose |
|---|---|---|
initialize | Deployer | Config PDA: admin, resolver, operator |
create_market | Operator | Market PDA + YES/NO mints + vault |
execute_trade | Taker + operator | Settle one maker leg |
mint_tokens | Anyone | Collateral → YES + NO |
merge_tokens | Anyone | YES + NO → collateral |
redeem | Holder | Winning tokens → collateral, 1:1 |
refund | Holder | Collateral back after cancellation |
withdraw_residual | Admin | Sweep a fully settled market |
close_market | Operator | Stop trading |
resolve_market | Resolution authority | Propose a resolution |
dispute_resolution | Anyone | Bond a dispute |
finalize_resolution | Anyone | Finalize after the window |
cancel_market | Config resolver | Void the market |
create_fee_vault | Admin | One-shot fee vault per collateral mint |
set_delegate | Maker | Approve the delegate PDA |
revoke_delegate | Maker | Revoke it |
update_config | Admin | Timelocked admin/resolver/operator rotation |
| PDA | Seeds | Purpose |
|---|---|---|
| Config | ["markets_config"] | Collateral mint, admin, resolver, operator, dispute params, fees, timelock |
| Market | ["market", market_id:u64 LE] | End time, mints, status, outcome, price accumulator |
| YES mint | ["yes_mint", market_pda] | Program-owned |
| NO mint | ["no_mint", market_pda] | Program-owned |
| Collateral vault | ["vault", market_pda] | Backs mint/merge/redeem/refund |
| Fee vault | ["fee_vault", mint] | Receives execute_trade fees |
| Delegate | ["delegate", maker] | The maker’s standing allowance |
execute_trade
Section titled “execute_trade”pub fn execute_trade( ctx: Ctx, side: Side, // the MAKER's side: 0 = BUY, 1 = SELL maker_amount: u64, taker_amount: u64, fee_rate_bps: u16, making: u64, // this fill: 0 < making <= maker_amount salt: u64, signer: Pubkey, order_taker: Pubkey, token_id: Vec<u8>, expiration: u64, nonce: u64, signature_type: u8, signature: [u8; 64],) -> Result<()>Fifteen accounts. Signers: taker, config operator, and the program (for the delegate PDA). The maker does not sign.
Constraints: signature_type == 0 (EOA), signer == maker, the order’s
taker must be public or the settling taker, a non-zero expiration must be in
the future, and a top-level ed25519 verify instruction must be present in
the same transaction.
Price accumulator
Section titled “Price accumulator”| Field | Meaning |
|---|---|
last_price_yes: u64 | Last executed YES price, 1e6 fp; init 500_000 |
price_cumulative_yes: u128 | Integral of last_price_yes × dt |
last_twap_ts: i64 | Last accrual time |
twap_anchor_{cur,prev}_{cum,ts} | Rolling anchors, 1-hour window |
total_volume: u64 | Collateral notional of settled fills |
Maintained only by execute_trade. → CLOB Markets as Legs
price = calculate_price(maker_amount, taker_amount, side);min_price = min(price, ONE - price);
Side::Buy => (fee_rate_bps * min_price * outcome_tokens) / (price * BPS_DIVISOR),Side::Sell => (fee_rate_bps * outcome_tokens / BPS_DIVISOR) * min_price / ONE,Charged on the taker asset. → Fees
#[event]pub struct TradeSettled { pub market_id: u64, pub side: Side, pub making: u64, pub taking: u64, pub fee: u64, pub maker: Pubkey, pub taker: Pubkey, pub signer: Pubkey, pub outcome_mint: Pubkey, pub order_hash: [u8; 32],}high-market-parlay
Section titled “high-market-parlay”Instructions
Section titled “Instructions”| Instruction | Who | Purpose |
|---|---|---|
initialize | Deployer, once | ParlayConfig + vault; pins the AMM program id, treasury, fee, bounds |
create_parlay | Any user | Open an N-leg slip; lock stake and probabilities |
settle_leg | Anyone | Mark a leg Won / Lost / Voided |
claim_parlay | Parlay owner | Pay out a Won slip |
void_parlay | Admin | Refund an unresolved Active slip, or recover a Won slip past its claim deadline |
seed_vault | Admin | Top up the vault |
propose_withdraw_vault | Admin | Queue a withdrawal; starts the timelock |
cancel_withdraw_vault | Admin | Abort a queued withdrawal during its window |
withdraw_vault | Admin | Execute the queued withdrawal after the timelock |
update_config | Admin | Mutate fee, max legs, stake bounds, max payout, paused, probability band, market-quality floors, claim deadline; proposes an admin rotation |
accept_admin | Pending admin | Complete the rotation after the timelock |
cancel_admin_rotation | Admin | Call off a pending rotation |
update_treasury | Admin | Rotate the fee destination (takes the account, not a pubkey) |
Two-step privileged actions
Section titled “Two-step privileged actions”Both admin rotation and vault withdrawal are timelocked at 48 hours in
production (ADMIN_ROTATION_TIMELOCK_SECS, WITHDRAW_TIMELOCK_SECS):
admin rotation update_config(new_admin) ──48h──▶ accept_admin │ (signed by the NEW admin) └── cancel_admin_rotation
withdrawal propose_withdraw_vault ──48h──▶ withdraw_vault │ (same amount + destination) └── cancel_withdraw_vaultThe point is that a stolen admin key cannot rotate-and-drain, or drain in one transaction — there is a window for the real admin to notice and cancel. Promotion requires a signature from the incoming admin, so a single-key compromise cannot complete a rotation on its own.
update_treasury is deliberately a separate instruction taking the new
treasury as an account, so the token::mint = config.collateral_mint
constraint rejects a wrong-mint account at admin time instead of bricking the
next user claim_parlay. Passing new_treasury to update_config is
rejected with UseUpdateTreasuryInstruction (6029).
create_parlay
Section titled “create_parlay”| Parameter | Type | Meaning |
|---|---|---|
legs | Vec<LegInput> | One { selected_outcome } per leg |
stake | u64 | Micro-USDC |
min_payout | u64 | Slippage floor |
user_seq | u64 | Per-user sequence; PDA seed |
remaining_accounts, length 2 × legs.len():
| Range | Accounts |
|---|---|
[0..N] | Market accounts, in leg order |
[N..2N] | MarketEventLink PDAs per leg’s market_id |
Per-leg guards: market Open; not past end_time (MarketExpired);
collateral mint matches the vault’s (CollateralMintMismatch); no duplicate
market_id; no two legs sharing an event_id; market age ≥
min_market_age_secs; b ≥ min_market_b; total_volume ≥ min_market_volume; probability inside the configured
[prob_floor_bps, prob_ceiling_bps] band.
Slip guards: 2 ≤ legs ≤ max_legs; stake within bounds;
combined_prob > 0; payout ≤ max_payout; payout ≥ min_payout; exposure
headroom; is_paused == false.
potential_payout = stake × HIGH_SCALE / combined_probcombined_prob = HIGH_SCALE × ∏(pᵢ / 1e6)HIGH_SCALE = 1e12The exposure check is computed against the vault balance including the incoming stake:
total_exposure + potential_payout ≤ 0.8 × (vault.amount + stake)settle_leg
Section titled “settle_leg”| Market state | Leg result |
|---|---|
| Resolved, user’s side won | Won; legs_won += 1 |
| Resolved, opposite side | Lost; parlay → Lost, exposure released |
| Cancelled | Voided; payout × prob_i / 1e6, exposure delta released |
| Anything else | Reverts — MarketNotResolved |
Permissionless. Idempotent through the LegNotPending check.
void_parlay
Section titled “void_parlay”Two distinct recovery paths, both ending in Refunded:
| Case | Condition |
|---|---|
| Unresolved Active slip | status == Active and no leg has resolved yet — otherwise ParlayHasResolvedLegs (6024) |
| Expired winner | status == Won and past parlay.claim_deadline — otherwise ClaimWindowNotElapsed (6026) |
The first is tightened against the older behaviour: once the user has any upside, an admin can no longer void them out of it. The second exists so a never-claimed winner doesn’t lock vault exposure permanently — the stake is refunded and the winnings are forfeited.
Refunds are always parlay.stake, always to parlay.user.
Accounts
Section titled “Accounts”ParlayConfig — singleton at ["parlay_config"].
| Field | Type | Notes |
|---|---|---|
admin | Pubkey | Squads multisig in production |
vault / treasury | Pubkey | Vault PDA; fee destination |
collateral_mint | Pubkey | Frozen at init; must have no freeze authority, except Circle USDC |
amm_program_id | Pubkey | Compile-time pinned, immutable |
fee_bps | u16 | ≤ MAX_FEE_BPS |
max_legs | u8 | |
min_stake / max_stake / max_payout | u64 | |
total_parlays | u64 | Monotonic; becomes parlay_id |
total_exposure | u64 | Sum of open potential_payout |
is_paused | bool | |
prob_floor_bps / prob_ceiling_bps | u16 | The per-leg probability band, in bps of PROB_SCALE |
min_market_age_secs | i64 | Must be ≥ TWAP_WINDOW_SECS |
min_market_b | u64 | Minimum AMM liquidity parameter |
min_market_volume | u64 | Minimum settled volume |
claim_deadline_secs | i64 | Claim window; valid range 1h – ~3y |
pending_admin / pending_admin_apply_at | Pubkey, i64 | Queued rotation |
pending_withdraw_{amount,destination,execute_after} | u64, Pubkey, i64 | Queued withdrawal |
bump / vault_bump | u8 |
Parlay — inline legs, no separate accounts.
| Field | Type |
|---|---|
parlay_id | u64 |
user | Pubkey |
stake | u64 |
potential_payout | u64 (mutable on the cancel path) |
legs_total / legs_resolved / legs_won | u8 |
status | ParlayStatus |
created_at | i64 |
fee_bps_at_create | u16 |
user_seq | u64 |
claim_deadline | i64 |
bump | u8 |
legs | Vec<ParlayLegData> |
ParlayLeg — 50 bytes each: market_id (u64), market_address (Pubkey),
selected_outcome (Outcome), probability_at_entry (u64, 1e6 = 1.0),
status (LegStatus).
Enums
ParlayStatus | Byte | Meaning |
|---|---|---|
| Active | 0 | At least one leg pending |
| Won | 1 | All legs terminal, none lost — claimable |
| Lost | 2 | A leg lost |
| Claimed | 3 | Paid out |
| Voided | 4 | Refund owed, still claimable once |
| Refunded | 5 | void_parlay already paid the stake back — terminal |
Refunded was appended deliberately so existing parlays keep their
numbering and nothing needs migrating.
LegStatus | Byte |
|---|---|
| Pending | 0 |
| Won | 1 |
| Lost | 2 |
| Voided | 3 |
Events
Section titled “Events”| Event | Payload |
|---|---|
ConfigInitialized | admin, vault, treasury, collateral_mint, amm_program_id, fee_bps, max_legs, stake bounds, max_payout |
ConfigUpdated | signer + post-update snapshot of every mutable field |
TreasuryUpdated | signer, previous_treasury, new_treasury |
VaultSeeded | admin, amount, new_balance |
VaultWithdrawn | admin, destination, amount, new_balance |
ParlayCreated | parlay_id, user, stake, potential_payout, legs_total, legs |
LegSettled | parlay_id, leg_index, market_id, won, voided |
ParlayClaimed | parlay_id, user, payout, fee |
ParlayVoided | parlay_id, user, refund |
ConfigUpdated carries the post-update snapshot so an indexer can overwrite
its mirror without diffing. amm_program_id is absent because it cannot
change.
Invariants
Section titled “Invariants”| # | Invariant | Enforced by |
|---|---|---|
| 1 | vault.amount ≥ total_exposure always | 80% rule + saturating decrements |
| 2 | total_exposure + new_payout ≤ 0.8 × (vault.amount + stake) at create | Explicit check |
| 3 | fee_bps_at_create set once, never mutated | Written at create, read at claim |
| 4 | Status transitions are one-way | Anchor constraint = status == X |
| 5 | A leg can’t settle before its market is terminal | is_resolved() ∥ is_cancelled() |
| 6 | (user, user_seq) used at most once | PDA init collision |
| 7 | void_parlay always refunds parlay.user | token::authority + explicit equality check |
| 8 | amm_program_id immutable after init | Compile-time constant, absent from update_config |
| 9 | Admin rotation and withdrawal need two signed steps 48h apart | Timelock fields + accept_admin signer |
Source
Section titled “Source”| Repo | Contents |
|---|---|
go-market-clob | The markets program + the four Rust services |
go-market-program | The AMM program + high-market-parlay |
go-market-api | Platform API (Django) — indexers and REST |
go-market-webapp | Frontend |
Utilities
Section titled “Utilities”| Tool | Purpose |
|---|---|
get-token | Generate a SIWS JWT from a keypair or seed phrase |
sign-order | Compute the order hash and sign it |
delegate | set / revoke / show the delegate allowance |
create-market | On-chain create_market + catalogue row |
devnet-* | Devnet wallets, tokens, init, lookup tables |