🚧 SportsPerp is currently live on devnet. Mainnet target: before Jun 12, 2026 (World Cup kickoff).

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):

FieldTypeMeaning
oracle_pricei64The current index value, fixed-point 10⁶ scale (500.0 → 500_000_000)
oracle_confidence_bpsu16Uncertainty interval in basis points (300 bps = ±3% implied)
oracle_timestampi64Unix seconds when this value was pushed
oracle_is_liveboolWhether 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:

ConfidenceMultiplierExample (on 5x market)
< 300 bps1.0x5x allowed
300–500 bps0.8x4x
500–800 bps0.6x3x
800–1000 bps0.4x2x
> 1000 bps0 (blocked)No new positions accepted

Implementation: confidence_leverage_multiplier in math/margin.rs.

Default confidence ranges at crank time:

StateConfidence pushed
Normal (between matches)300 bps
Live match600 bps

The pusher uses these flat values for both teams and players in v1 (DEFAULT_CONFIDENCE_BPS = 300, LIVE_CONFIDENCE_BPS = 600 in oracle-pusher/index.mjs). A per-market-type confidence policy (wider intervals for players to reflect higher volatility) is on the post-launch tuning list but not implemented today; the on-chain oracle_confidence_bps field accepts any value, so the change is configuration-only when it ships.

Live-match confidence is doubled relative to normal to reflect the additional uncertainty of in-flight data — wider intervals trigger the confidence-based leverage throttle automatically.

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_price

Samples accumulate across the 8-hour funding window. At the next apply_funding call, the program:

  1. Averages the accumulated samples (plain mean of premium_accumulator / premium_sample_count).
  2. Normalises by oracle_price to get the ratio.
  3. 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. (Outlier rejection on the samples is not implemented in v1 and is tracked as a potential future hardening.) See Funding Rate.

Single-source today, multi-source tomorrow

Today (devnet)

Single-source: the admin keypair signs each update_oracle. This is acceptable for devnet but is the single biggest centralization vector in the current design. An admin-key compromise would allow arbitrary price manipulation until the keys were rotated.

Mitigations in place:

  • Composite mark price — even if the oracle is manipulated, the vAMM weight (50% live / 70% between) dampens the impact. The 150-second EMA dampens further.
  • ±5% premium check — new positions cannot open if mark deviates > 5% from oracle, blocking profit extraction from a manipulated state.
  • Confidence-based leverage — wider confidence → lower max leverage, limiting exposure at high-uncertainty moments.
  • Keys on Hetzner, not in repo. Admin keypair lives in a systemd EnvironmentFile; ~/.config/solana/id.json on the server. Never committed.

Mainnet path: multi-source 2-of-N

oracle-pusher/multi-oracle.mjs is already built (588 lines), supporting:

  • Multiple independent pushers — each running on a separate machine with a separate key.
  • Weighted median consensusupdate_oracle accepts 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.

Deployment is gated on:

  • Second Hetzner node (or another VPS) for the redundant pusher.
  • Switchboard PullFeeds integration as a third source — adds TEE (Trusted Execution Environment) security on top of the 2-of-N.

Target: live before mainnet. See Roadmap.

Circuit breaker on extreme moves

A single oracle push that moves the price by > 10% from the previous value triggers a circuit breaker: the new confidence interval auto-widens. Leverage reductions follow from the confidence multiplier automatically.

The program does not reject the push — a genuine large move should be reflected. But it tightens risk controls while the market digests the move.

What the oracle is not responsible for

Some jobs that seem oracle-adjacent are handled elsewhere:

  • Computing the OBV composite. Off-chain in engine/index-calculator.ts. 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_oi vs total_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_oracle

Or 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.