Account Structure (PDAs)
Reference for all 7 PDA account types the program maintains. Every field is sourced from programs/obv-perps/src/state/.
1. MarketConfig
Per-market configuration + live state. One per market (68 total at launch).
Seeds: ["market", market_id: u16]
| Field | Type | Scale | Purpose |
|---|---|---|---|
market_id | u16 | — | 0–19 teams, 20–99 players (sparse) |
market_type | u8 | — | 0 = Team, 1 = Player |
admin | Pubkey | — | Authorized caller for admin instructions |
max_leverage | u16 | 10² | Market cap (500 = 5x, 200 = 2x) |
taker_fee_bps / maker_fee_bps | u16 | 10⁴ | 10 bps / 5 bps at launch |
funding_interval | i64 | seconds | 28800 (8h) |
max_funding_rate_bps | u16 | 10⁴ | ±10 bps per period |
paused | bool | — | Emergency halt |
total_long_oi / total_short_oi | u64 | 10⁶ | Open interest |
cumulative_funding_long / cumulative_funding_short | i64 | 10¹² | Per-unit funding counters |
last_funding_time | i64 | Unix s | Last apply_funding |
| Oracle fields | |||
oracle_price | i64 | 10⁶ | Latest pushed index |
oracle_confidence_bps | u16 | 10⁴ | Uncertainty interval |
oracle_timestamp | i64 | Unix s | Last push time |
oracle_is_live | bool | — | Whether match data is driving updates |
| TWAP for funding | |||
premium_accumulator | i128 | 10⁶ | Running sum of (mark − oracle) samples |
premium_sample_count | u32 | — | Sample count since last funding |
last_premium_sample | i64 | Unix s | Last sample time |
| Composite mark | |||
mark_price | i64 | 10⁶ | Composite (oracle + vAMM, no EMA) |
mark_price_ema | i64 | 10⁶ | 150s EMA — the canonical settlement price |
last_mark_update | i64 | Unix s | Last EMA computation |
vamm_impact_factor | u32 | 10⁶ | Default 1000 (0.1%) |
oracle_weight_live_bps | u16 | 10⁴ | Default 5000 (50%) |
oracle_weight_between_bps | u16 | 10⁴ | Default 3000 (30%) |
| Layer 3 + sunset | |||
adl_enabled | bool | — | Layer 3 permitted |
settled | bool | — | Post-sunset lock flag |
settlement_price | i64 | 10⁶ | Fixed at sunset time |
settled_at | i64 | Unix s | 0 = not settled |
bump | u8 | — | PDA bump |
2. UserPosition
One per trader per market. Created by open_position; closed by close_position, liquidation, or settlement.
Seeds: ["position", market_config: Pubkey, owner: Pubkey]
The first key is the MarketConfig PDA, not the raw market id. Pre-derive it from ["market", market_id] and pass that pubkey when deriving the position PDA.
| Field | Type | Scale | Purpose |
|---|---|---|---|
owner | Pubkey | — | Position owner |
market | Pubkey | — | The market PDA |
direction | u8 | — | 0 = Long, 1 = Short |
size | u64 | 10⁶ | Notional size in USDC |
entry_price | i64 | 10⁶ | Mark EMA at open time |
collateral | u64 | 10⁶ | USDC posted |
cumulative_funding_at_entry | i64 | 10¹² | Funding snapshot at open |
opened_at | i64 | Unix s | Open timestamp |
last_margin_transfer_collateral | u64 | 10⁶ | Collateral at last margin op (anti-manipulation baseline) |
last_margin_transfer_time | i64 | Unix s | Last margin op |
last_partial_liquidation_time | i64 | Unix s | Last Layer 1 hit (30s cooldown) |
bump | u8 | — | PDA bump |
One position per (trader, market) pair is enforced by the PDA seeds. A trader who wants two positions on the same market must use two different signers (e.g., a sub-account).
3. LiquidityPool
Singleton. Global cross-market USDC custody — one PDA + one SPL token account shared by all 68 markets. Replaces the per-market VaultAccount model that earlier versions used.
Seeds: ["liquidity_pool"]
| Field | Type | Purpose |
|---|---|---|
admin | Pubkey | Authorized for seed_pool and withdraw_pool_lp |
total_lp_deposits | u64 | Lifetime LP deposits (informational; canonical balance is pool_token.amount) |
total_lp_withdrawals | u64 | Lifetime LP withdrawals |
token_mint | Pubkey | USDC mint |
token_account | Pubkey | The actual pool_token SPL token account holding USDC for every market |
risk_floor | u64 | Hard on-chain gate. Below it, open_position and LimitOpen revert with RiskFloorBreached. Closes / SL / TP / cancels / margin-passing collateral withdraws are not gated. Also enforced on withdraw_pool_lp |
target_balance | u64 | Off-chain alert threshold (read by monitoring; [warning] at < 2×, [error] at < 1.25×). Not enforced on-chain |
bump | u8 | PDA bump |
The pool_token SPL token account is at seeds ["pool_token"] with token::authority = liquidity_pool (the LiquidityPool PDA). Per-market accounting (open interest, funding accumulators, mark price) still lives on MarketConfig; only capital custody is global.
The full solvency invariant pool_token.amount ≥ Σ obligations is enforced off-chain by scripts/probe-solvency.ts (60s cadence with Telegram alerts on drift). On-chain instructions do cheap local checks only — see On-Chain Program → Capital model.
4. InsuranceFund
Global insurance fund. One PDA, shared across all markets.
Seeds: ["insurance"]
| Field | Type | Purpose |
|---|---|---|
admin | Pubkey | Authorized for admin instructions |
token_mint | Pubkey | USDC mint |
token_account | Pubkey | Holds the USDC balance |
total_balance | u64 | Current USDC (redundant with token_account.amount; kept for o11y) |
max_backstop_exposure | u64 | Cap on outstanding absorbed positions |
current_backstop_exposure | u64 | Live — decreases on unwind |
total_absorbed | u64 | Lifetime counter (monotonic) |
total_unwound | u64 | Lifetime counter (monotonic) |
target_balance | u64 | Withdrawal floor — admin cannot go below |
bump | u8 | PDA bump |
5. BackstopPosition
Created when Layer 2 absorbs an underwater position. Lives until fully unwound. At most one outstanding BackstopPosition per market at a time — the next absorption on the same market reuses the PDA after the previous one is fully unwound.
Seeds: ["backstop", market_id: u16]
| Field | Type | Scale | Purpose |
|---|---|---|---|
market | Pubkey | — | The MarketConfig PDA |
market_id | u16 | — | |
direction | u8 | — | 0 = Long, 1 = Short (mirror of original position) |
size | u64 | 10⁶ | Remaining unwound size |
entry_price | i64 | 10⁶ | Mark price at absorption |
created_at | i64 | Unix s | Absorption time |
bump | u8 | — | PDA bump |
6. TriggerOrder
One per placed SL, TP, or Limit Open. Closed on execute, cancel, or expiry-triggered cleanup.
Seeds: ["trigger", market_config: Pubkey, owner: Pubkey, order_id: u64 (LE)]
The first key is the MarketConfig PDA (not the raw market id), and the order id is serialized as little-endian u64.
| Field | Type | Scale | Purpose |
|---|---|---|---|
owner | Pubkey | — | Order creator |
market | Pubkey | — | Market PDA |
market_id | u16 | — | Duplicated for convenience |
order_id | u32 | — | User-scoped monotonic index |
order_type | u8 | — | 0 = StopLoss, 1 = TakeProfit, 2 = LimitOpen |
direction | u8 | — | 0 = Long, 1 = Short |
trigger_price | i64 | 10⁶ | Fire threshold |
trigger_condition | u8 | — | 0 = Above, 1 = Below |
size | u64 | 10⁶ | Position size (LimitOpen) or close amount (SL/TP) |
collateral | u64 | 10⁶ | Escrowed (LimitOpen only) |
leverage | u16 | 10³ | LimitOpen only |
reduce_only | bool | — | Required true for SL/TP |
expiry | i64 | Unix s | 0 = never |
created_at | i64 | Unix s | Placement time |
bump | u8 | — | PDA bump |
Max 10 TriggerOrder accounts per (user, market) pair.
7. UserOrderCounter
Monotonic order ID allocator per user.
Seeds: ["order_counter", owner: Pubkey, market_config: Pubkey]
One counter per (user, market) pair — independent monotonic sequences keep order ids tightly packed within a market without colliding across markets.
| Field | Type | Purpose |
|---|---|---|
owner | Pubkey | User |
market | Pubkey | The MarketConfig PDA scope |
next_order_id | u64 | Next order_id to assign (used as the u64-LE seed of the next TriggerOrder) |
bump | u8 | PDA bump |
next_order_id never decrements — cancelled orders don’t free their slot for reuse. This guarantees no two of a user’s orders ever share a PDA.
Deriving PDAs from TypeScript
import { PROGRAM_ID } from "@sportsperp/sdk";
import { PublicKey } from "@solana/web3.js";
import { struct, u16 } from "@coral-xyz/anchor";
// Market
const [marketPda] = PublicKey.findProgramAddressSync(
[Buffer.from("market"), new BN(marketId).toArrayLike(Buffer, "le", 2)],
PROGRAM_ID
);
// Position
const [positionPda] = PublicKey.findProgramAddressSync(
[Buffer.from("position"), marketPda.toBuffer(), user.toBuffer()],
PROGRAM_ID
);Or just use the SDK helpers (sdk/src/pda.ts) — they’re exactly the same derivations, with typed signatures.
Account sizes (approximate)
For rent calculations and bulk queries:
| Account | Approximate size |
|---|---|
MarketConfig | ~350 bytes |
UserPosition | ~138 bytes |
LiquidityPool | ~120 bytes |
InsuranceFund | ~200 bytes |
BackstopPosition | ~140 bytes |
TriggerOrder | ~160 bytes |
UserOrderCounter | ~50 bytes |
Tip: When using getProgramAccounts with memcmp filters, match on dataSize: 138 to find UserPositions, dataSize: 350 for MarketConfigs, etc. Don’t fetch without filters — devnet will rate-limit.
Further reading
- SDK — the account fetcher helpers that wrap this state.
- Protocol Architecture → On-Chain Program — the program’s view of these accounts.
- Fixed-Point Math — the scales referenced throughout this table.