Skip to content

On-Chain Programs

Three Anchor programs:

ProgramIDRole
high-market-program4Pe4r9Fpa…The LMSR AMM — live on mainnet
marketsGoFTc9SFq…CLOB market lifecycle and settlement
high-market-parlay3iZaRuW8D…GoCombo multi-leg slips

→ Two Venues · Contracts & Audits

The venue in production. Pricing is fully on-chain — there is no matching layer and no off-chain service in the trade path.

InstructionWhoPurpose
initializeDeployerConfig PDA
create_marketAdmin (multisig)Market PDA + YES/NO mints + USDC vault
buy_sharesAnyoneBuy YES or NO against the curve
sell_sharesAnyoneSell back to the curve
claim_winningsHolderWinning shares → USDC after resolution
lifecycleAdmin / anyoneClose, resolve, dispute, finalize, cancel, propagate event resolution
AccountSeeds
Config["config"]
Market["market", market_id]
YES / NO mint["yes_mint" | "no_mint", market_key]
Vault["vault", market_key]
Event / MarketEventLinkper 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.

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.

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.


InstructionWhoPurpose
initializeDeployerConfig PDA: admin, resolver, operator
create_marketOperatorMarket PDA + YES/NO mints + vault
execute_tradeTaker + operatorSettle one maker leg
mint_tokensAnyoneCollateral → YES + NO
merge_tokensAnyoneYES + NO → collateral
redeemHolderWinning tokens → collateral, 1:1
refundHolderCollateral back after cancellation
withdraw_residualAdminSweep a fully settled market
close_marketOperatorStop trading
resolve_marketResolution authorityPropose a resolution
dispute_resolutionAnyoneBond a dispute
finalize_resolutionAnyoneFinalize after the window
cancel_marketConfig resolverVoid the market
create_fee_vaultAdminOne-shot fee vault per collateral mint
set_delegateMakerApprove the delegate PDA
revoke_delegateMakerRevoke it
update_configAdminTimelocked admin/resolver/operator rotation
PDASeedsPurpose
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
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.

FieldMeaning
last_price_yes: u64Last executed YES price, 1e6 fp; init 500_000
price_cumulative_yes: u128Integral of last_price_yes × dt
last_twap_ts: i64Last accrual time
twap_anchor_{cur,prev}_{cum,ts}Rolling anchors, 1-hour window
total_volume: u64Collateral 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],
}

InstructionWhoPurpose
initializeDeployer, onceParlayConfig + vault; pins the AMM program id, treasury, fee, bounds
create_parlayAny userOpen an N-leg slip; lock stake and probabilities
settle_legAnyoneMark a leg Won / Lost / Voided
claim_parlayParlay ownerPay out a Won slip
void_parlayAdminRefund an unresolved Active slip, or recover a Won slip past its claim deadline
seed_vaultAdminTop up the vault
propose_withdraw_vaultAdminQueue a withdrawal; starts the timelock
cancel_withdraw_vaultAdminAbort a queued withdrawal during its window
withdraw_vaultAdminExecute the queued withdrawal after the timelock
update_configAdminMutate fee, max legs, stake bounds, max payout, paused, probability band, market-quality floors, claim deadline; proposes an admin rotation
accept_adminPending adminComplete the rotation after the timelock
cancel_admin_rotationAdminCall off a pending rotation
update_treasuryAdminRotate the fee destination (takes the account, not a pubkey)

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_vault

The 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).

ParameterTypeMeaning
legsVec<LegInput>One { selected_outcome } per leg
stakeu64Micro-USDC
min_payoutu64Slippage floor
user_sequ64Per-user sequence; PDA seed

remaining_accounts, length 2 × legs.len():

RangeAccounts
[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_prob
combined_prob = HIGH_SCALE × ∏(pᵢ / 1e6)
HIGH_SCALE = 1e12

The exposure check is computed against the vault balance including the incoming stake:

total_exposure + potential_payout ≤ 0.8 × (vault.amount + stake)

→ Pricing & Payout Math

Market stateLeg result
Resolved, user’s side wonWon; legs_won += 1
Resolved, opposite sideLost; parlay → Lost, exposure released
CancelledVoided; payout × prob_i / 1e6, exposure delta released
Anything elseReverts — MarketNotResolved

Permissionless. Idempotent through the LegNotPending check.

Two distinct recovery paths, both ending in Refunded:

CaseCondition
Unresolved Active slipstatus == Active and no leg has resolved yet — otherwise ParlayHasResolvedLegs (6024)
Expired winnerstatus == 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.

ParlayConfig — singleton at ["parlay_config"].

FieldTypeNotes
adminPubkeySquads multisig in production
vault / treasuryPubkeyVault PDA; fee destination
collateral_mintPubkeyFrozen at init; must have no freeze authority, except Circle USDC
amm_program_idPubkeyCompile-time pinned, immutable
fee_bpsu16≤ MAX_FEE_BPS
max_legsu8
min_stake / max_stake / max_payoutu64
total_parlaysu64Monotonic; becomes parlay_id
total_exposureu64Sum of open potential_payout
is_pausedbool
prob_floor_bps / prob_ceiling_bpsu16The per-leg probability band, in bps of PROB_SCALE
min_market_age_secsi64Must be ≥ TWAP_WINDOW_SECS
min_market_bu64Minimum AMM liquidity parameter
min_market_volumeu64Minimum settled volume
claim_deadline_secsi64Claim window; valid range 1h – ~3y
pending_admin / pending_admin_apply_atPubkey, i64Queued rotation
pending_withdraw_{amount,destination,execute_after}u64, Pubkey, i64Queued withdrawal
bump / vault_bumpu8

Parlay — inline legs, no separate accounts.

FieldType
parlay_idu64
userPubkey
stakeu64
potential_payoutu64 (mutable on the cancel path)
legs_total / legs_resolved / legs_wonu8
statusParlayStatus
created_ati64
fee_bps_at_createu16
user_sequ64
claim_deadlinei64
bumpu8
legsVec<ParlayLegData>

ParlayLeg — 50 bytes each: market_id (u64), market_address (Pubkey), selected_outcome (Outcome), probability_at_entry (u64, 1e6 = 1.0), status (LegStatus).

Enums

ParlayStatusByteMeaning
Active0At least one leg pending
Won1All legs terminal, none lost — claimable
Lost2A leg lost
Claimed3Paid out
Voided4Refund owed, still claimable once
Refunded5void_parlay already paid the stake back — terminal

Refunded was appended deliberately so existing parlays keep their numbering and nothing needs migrating.

LegStatusByte
Pending0
Won1
Lost2
Voided3
EventPayload
ConfigInitializedadmin, vault, treasury, collateral_mint, amm_program_id, fee_bps, max_legs, stake bounds, max_payout
ConfigUpdatedsigner + post-update snapshot of every mutable field
TreasuryUpdatedsigner, previous_treasury, new_treasury
VaultSeededadmin, amount, new_balance
VaultWithdrawnadmin, destination, amount, new_balance
ParlayCreatedparlay_id, user, stake, potential_payout, legs_total, legs
LegSettledparlay_id, leg_index, market_id, won, voided
ParlayClaimedparlay_id, user, payout, fee
ParlayVoidedparlay_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.

#InvariantEnforced by
1vault.amount ≥ total_exposure always80% rule + saturating decrements
2total_exposure + new_payout ≤ 0.8 × (vault.amount + stake) at createExplicit check
3fee_bps_at_create set once, never mutatedWritten at create, read at claim
4Status transitions are one-wayAnchor constraint = status == X
5A leg can’t settle before its market is terminalis_resolved() ∥ is_cancelled()
6(user, user_seq) used at most oncePDA init collision
7void_parlay always refunds parlay.usertoken::authority + explicit equality check
8amm_program_id immutable after initCompile-time constant, absent from update_config
9Admin rotation and withdrawal need two signed steps 48h apartTimelock fields + accept_admin signer

RepoContents
go-market-clobThe markets program + the four Rust services
go-market-programThe AMM program + high-market-parlay
go-market-apiPlatform API (Django) — indexers and REST
go-market-webappFrontend
ToolPurpose
get-tokenGenerate a SIWS JWT from a keypair or seed phrase
sign-orderCompute the order hash and sign it
delegateset / revoke / show the delegate allowance
create-marketOn-chain create_market + catalogue row
devnet-*Devnet wallets, tokens, init, lookup tables