Oracle Design
The oracle is the protocol’s contact point with the outside world — the component that says “this is the current OBV Index for market X.” If the oracle is compromised, everything downstream is too. This page documents the current devnet design, the mainnet multi-source path, and the specific properties that make the oracle hard to game.
The oracle’s job, in three fields
Every on-chain market stores three oracle-related fields (MarketConfig):
| Field | Type | Meaning |
|---|---|---|
oracle_price | i64 | The current index value, fixed-point 10⁶ scale (500.0 → 500_000_000) |
oracle_confidence_bps | u16 | Uncertainty interval in basis points (300 bps = ±3% implied) |
oracle_timestamp | i64 | Unix seconds when this value was pushed |
oracle_is_live | bool | Whether a live match is actively driving updates |
All four are updated atomically by the update_oracle instruction. Downstream logic — margin checks, mark-price computation, funding, trigger evaluation — reads all four as a consistent tuple.
Confidence, not “just a price”
Most DeFi oracles publish a single price. SportsPerp publishes a price + confidence interval for every market, and the program uses both.
What confidence represents
Confidence captures the model’s uncertainty about the current value. It is driven by:
- How recent the underlying data is. Fresh post-match REST data → narrow interval. Mid-match live estimate → wider.
- Whether the composite is in a regime transition. At the start of a live match, before the overlay has established a trend, the interval widens.
- Data pipeline tier. Authoritative (from the XGBoost sidecar) → narrow; aggregated → wider; heuristic → wider still.
How the program uses confidence
Confidence scales down max leverage automatically: the wider the published interval, the lower the maximum leverage the program will permit on a new position, stepping down through several bands until, beyond a high-uncertainty threshold, new positions are refused entirely. The exact band boundaries are part of the on-chain margin math.
Confidence is widened during live matches relative to between-match conditions, reflecting the additional uncertainty of in-flight data — wider intervals trigger the leverage throttle automatically. Per-market-type confidence tuning is on the post-launch roadmap.
On-chain update guards (deviation cap + rate limit)
The off-chain pusher’s consensus rules alone cannot constrain a stolen oracle_authority key — an attacker holding the key could call update_oracle directly. The program therefore enforces both guards on-chain:
- Per-update deviation cap — a new price may differ from the last on-chain price by at most 20% (2000 bps), with the allowance floored at one index point so the gate can never deadlock a market. Legitimate larger moves (e.g. after a long pusher outage) are reached by walking in capped steps across successive updates — the deployed pusher does this automatically.
- Minimum update interval — at most one accepted update per market per 60 seconds (the normal push cadence is 300 seconds).
Together these bound a compromised hot key to ~20% of price movement per minute instead of an arbitrary instant repricing — enough time for monitoring to page and for the admin to rotate the key via set_oracle_authority. Out-of-band updates reject with OracleDeviationTooLarge / OracleUpdateTooFrequent.
One property worth stating plainly: a compromised key streaming updates every 60 seconds can also starve the honest pusher through the interval gate (the honest updates arrive mid-interval and reject). The guards do not try to prevent that — key compromise always ends in rotation — they bound the price damage per unit time while it is detected. The pusher surfaces its guard rejections in /health (guardRejections) precisely so an unexpected stream of them pages operations.
The update guards ship in the 59-error program build (June 2026); the devnet upgrade accompanies this release.
Staleness and the 2-hour cutoff
Every on-chain operation that depends on the oracle checks freshness:
require!(
(current_time - market.oracle_timestamp) <= MAX_ORACLE_STALENESS,
OBVPerpsError::OracleStale
);
// MAX_ORACLE_STALENESS = 7200 seconds (2 hours)Affects: open_position, close_position (at close), add_collateral, withdraw_collateral, place_trigger_order, execute_trigger_*, apply_funding.
The practical effect: after 2 hours of no oracle pushes, trading pauses automatically for that market. Existing positions remain intact; they can be closed at the last known mark EMA, but no new positions can open and no trigger orders fire.
Two reasons for the specific 2-hour threshold:
- Shorter than a typical match cycle. Matchday windows are 4–6 hours; a market that ceased updating mid-match would be flagged and paused long before the match ended.
- Long enough to absorb normal gaps. The 5-minute cadence has ~24 pushes in 2 hours; a network hiccup or brief upstream-feed outage won’t trigger stale-oracle rejections.
For markets quiet longer than 72 hours, the sunset mechanism kicks in — anyone can call sunset_market to start settlement.
TWAP sampling for funding
The oracle doesn’t only push a price — each update_oracle call also contributes a premium sample for funding rate computation. The on-chain code stores the absolute delta:
premium_sample = mark_price_ema - oracle_priceSamples accumulate across the 8-hour funding window. At the next apply_funding call, the program:
- Averages the accumulated samples (plain mean of
premium_accumulator / premium_sample_count). - Normalises by
oracle_priceto get the ratio. - Clamps the result to ±0.1% (10 bps).
This moves funding manipulation from a single-block attack to a sustained multi-hour attack — a materially harder ask. Further sample-level statistical hardening is on the post-launch roadmap. See Funding Rate.
Centralized today, decentralized tomorrow
Today (devnet)
The deployed pusher already runs 2-of-N weighted-median consensus (multi-oracle.mjs) across two candle servers, signed by a dedicated hot oracle_authority (separate from the admin since the B5 cutover). But both consensus legs are SportsPerp-derived — same data, same operator — so this is server redundancy, not trust decentralization: a single-party compromise can still reach the chain. That remaining single-party trust is the biggest centralization vector in the current design, and removing it (a fully independent second data feed plus an admin multi-sig) is a mainnet launch gate.
Mitigations in place today:
- Composite mark price — even if the oracle is off, the vAMM weight dampens the impact, and the 150-second EMA dampens further.
- ±5% premium check — new positions cannot open if mark deviates materially from oracle, blocking profit extraction from a dislocated state.
- Confidence-based leverage — wider confidence → lower max leverage, limiting exposure at high-uncertainty moments.
- Hardened key custody. Signing keys are held server-side in a restricted secrets store, never committed to source control, with offline backups under separate custody.
Mainnet path: multi-source 2-of-N
A multi-source consensus pusher is deployed on devnet and on the mainnet path, supporting:
- Multiple independent pushers — each running on separate infrastructure with a separate key.
- Weighted median consensus —
update_oracleaccepts a value only if ≥ 2 of N sources agree within a tolerance band. - Reputation weighting — long-running sources with track records of accuracy carry more weight.
Mainnet deploys the 2-of-N weighted-median consensus as the oracle authority. A TEE-backed third source (Switchboard PullFeeds) is an optional post-launch enhancement layered on top of the 2-of-N — not a mainnet gate. See Roadmap.
Circuit breaker on extreme moves
A single oracle push that moves the price sharply from the previous value triggers a circuit breaker: the confidence interval auto-widens, and the leverage throttle tightens risk controls automatically while the market digests the move. The design intent is that genuine large moves are still reflected (sports performance can legitimately gap) while the surrounding risk controls clamp down — the precise behaviour is enforced in the on-chain oracle path.
What the oracle is not responsible for
Some jobs that seem oracle-adjacent are handled elsewhere:
- Computing the OBV composite. Off-chain in the index calculator. The program receives a finished index value, not raw OBV + form + results.
- Choosing the vAMM component of the mark. Derived on-chain from
total_long_oivstotal_short_oi, not from the oracle. - Liquidator candidate selection. Off-chain keeper; on-chain program only validates the submitted target.
Keeping the oracle narrow — price + confidence + liveness + TWAP sample — keeps the trust surface small. Anything else is either on-chain determinism or off-chain service responsibility, not oracle concern.
Auditable timeline
Every oracle push is on-chain. Anyone can reconstruct the full history for a market:
solana transaction-history 6d4fSCD7mNy7aDNS2mXUxYpZjFFQKBKwAsM5kojKQA6h \
--limit 1000 | grep update_oracleOr via the @sportsperp/sdk event parser, which surfaces OraclePriceUpdated events with price, confidence, liveness flag, and block time. This is a public audit trail — no off-chain log or database is required to see what the oracle has claimed.
Further reading
- Funding Rate — how TWAP samples become a settled funding payment.
- Mark Price — the composite that blends the oracle with on-chain vAMM signal.
- Off-Chain Services — the oracle crank and pusher that produce on-chain updates.