Protocol Overview

Oracles

How Project 0 prices assets with Pyth, Switchboard, Scope and exchange-rate oracles, and how confidence intervals, EMA and staleness apply.

Each Bank is configured with an oracle setup by the Group administrator. The setup names the price source stored in bank.config.oracle_keys[0] and, for some setups, one or two extra on-chain accounts the program reads alongside it to derive the final price.

Oracle setups

SetupPrice source (oracle_keys[0])Extra accountsHow the price is formed
PythPythPushOracle, and the KaminoPythPush / DriftPythPull / SolendPythPull / JuplendPythPull venue variantsPyth price update accountVenue reserve / market in oracle_keys[1] for the venue variantsPyth price and confidence, kept fresh by Pyth's own infrastructure; no caller action.
SwitchboardSwitchboardPull and its venue variantsSwitchboard pull feedVenue account in oracle_keys[1] for the venue variantsFeed value; the caller must crank the feed just before use.
ScopeScopeA Scope OraclePrices account; bank.config.scope_entry_index selects one of its 512 entriesNoneThe entry's value / 10^exp. Scope carries no confidence interval; risk is expressed through the bank's weights. Program 0.1.11.
mSOLPythMSOL, KaminoMSOL, JuplendMSOLPyth SOL/USDMarinade State (oracle_keys[1]; oracle_keys[2] on the venue variants, whose [1] is the venue account)SOL/USD × the mSOL/SOL rate derived from Marinade's virtual staked balance over the mSOL supply. Program 0.1.11.
LSTPythLST, KaminoLST, JuplendLSTPyth SOL/USDSPL or Sanctum stake pool (same slots as mSOL)SOL/USD × total_lamports / pool_token_supply. The pool must have been updated in the current or previous epoch. Program 0.1.11.
Exponent PTPTPyth, PTFixedPyth base feed for PTPyth; none for PTFixed (the underlying is treated as $1)Exponent vault (oracle_keys[1] for PTPyth, oracle_keys[0] for PTFixed)Base × a linear rate that accretes from the bank's configured start price to par at the vault's maturity, capped by what the PT can actually redeem for. Program 0.1.11.
FixedFixed and its venue variantsNoneVenue account for the venue variantsAn administrator-set constant (rare, special cases).

For the mSOL, LST and PT setups the exchange rate is part of the oracle price itself — an mSOL bank's price is mSOL/USD. The receipt-token rate of a venue (a Kamino cToken, a JupLend fToken) is a separate step applied on top by the risk engine, exactly as it is for every other venue bank.

Oracle Confidence Interval Adjustment

Some oracles report a price with a confidence interval (P +/- c), representing the range within which the true price likely falls. P0 uses confidence intervals conservatively:

  • Assets (collateral) are priced at the lower bound: P - P * c
  • Liabilities (debt) are priced at the upper bound: P + P * c

Example: If the oracle reports Token A at $20 with a 5% confidence interval:

  • As collateral, Token A is valued at $20 - $1 = $19
  • As debt, Token A is valued at $20 + $1 = $21

The confidence interval is capped at a maximum of 5%. If the oracle reports a wider confidence interval, the protocol may clamp it to 5% or abort the transaction entirely. This is relatively unique to P0 in the borrow-lending space and provides an additional layer of protection against oracle instability. Scope prices and fixed prices have no confidence interval, so their collateral and liability values coincide.

EMA vs Spot Price

Some oracles report both a spot price (the current instantaneous price) and an Exponential Moving Average (EMA) price (a price-over-time instead of an instant price "right now").

When both are available, P0 uses them strategically:

OperationPrice Used
BorrowingEMA price (more stable, harder to manipulate)
LiquidationSpot price (more responsive to rapid changes)

If an EMA price is not available, all operations use the spot price.

Staleness

Oracle data must be fresh: every bank has an oracle_max_age (seconds), and a transaction that depends on an older price fails.

  • Pyth oracles are rarely stale since Pyth maintains its own update cadence.
  • Switchboard oracles require the caller to send a crank instruction just before the transaction to refresh the data.
  • Scope entries are refreshed by the feed operator; there is nothing for a caller to crank. A bank's oracle_max_age is applied to the entry's own timestamp with no default — an age of 0 is always stale.
  • mSOL, LST and PT setups inherit the freshness of their Pyth base feed. A stake pool additionally counts as stale if it has not been updated in the current or previous epoch.

When composing transactions that use Switchboard oracles, either bundle the crank instruction in the same transaction (if compute and account limits allow) or send a separate crank transaction with a brief delay before the main transaction. Many operators use Jito bundles for this.

Oracle Configuration

Each Bank's oracle configuration is readable from bank.config.oracle_setup, bank.config.oracle_keys and, for Scope banks, bank.config.scope_entry_index.

When constructing a transaction that requires a risk engine check, every active bank contributes its accounts to the remaining accounts in this order — the bank, then its oracle accounts exactly as the program expects them:

SetupAccounts per bank
Pyth, Switchboard, Scope, Fixed (plain)bank, oracle_keys[0]
Any venue variant (Kamino, Drift, Solend, JupLend)bank, oracle_keys[0], oracle_keys[1] (venue account)
PythMSOL, PythLST, PTPythbank, oracle_keys[0], oracle_keys[1] (Marinade State / stake pool / vault)
KaminoMSOL, JuplendMSOL, KaminoLST, JuplendLSTbank, oracle_keys[0], oracle_keys[1] (venue account), oracle_keys[2] (Marinade State / stake pool)
PTFixedbank, oracle_keys[0] (the vault)
Native stake (StakedWithPythPush)bank, oracle_keys[0], oracle_keys[1], oracle_keys[2], and the pool on-ramp

The TypeScript SDK builds this list for you (computeHealthAccountMetas); see Program Upgrade 0.1.11 if you assemble it yourself. For Kamino specifically, refresh_reserve must also execute within the same slot.

On this page