The Oracle Program delivers accurate price data to the protocol by integrating with trusted oracle providers. It calculates exchange rates by combining multiple price sources in a structured sequence, ensuring reliable asset valuations.
NOTEThe Oracle program has been audited by Zenith and Offside.Oracle Program Address: jupnw4B6Eqs7ft6rxpzYLJZYSnrpRgPcr589n5Kv4oc
Hop-Based Oracle System
The system computes exchange rates by processing prices from up to four sources in a sequential chain. Each source contributes to the final rate through multiplication or division, with the option to invert values as needed. The output of each hop becomes the input for the next.
For example, to derive the JUPSOL/SOL rate, the system combines JUPSOL/USD and SOL/USD feeds, inverting the latter to obtain USD/SOL.
Supported source types
| Source | Description | Accounts per hop |
|---|
| Pyth | Pyth push oracle feeds | 1 |
| Chainlink | Chainlink Solana feeds | 1 |
| Redstone | Redstone oracle feeds | 1 |
| StakePool | SPL Stake Pool (Jito, Marinade, etc.) | 1 |
| MsolPool | Marinade mSOL pool state | 1 |
| SinglePool | SPL Single Pool (Helius, Jupiter, Nansen, Emerald, Shift, Kiln) | 3 (stake, mint, stake program) |
| JupLend | JupLend fToken exchange rate | 4 (Lending, TokenReserve, RateModel, FTokenMint) |
This design enables the system to:
- Aggregate rates from multiple feeds and providers, reducing dependency on any single source
- Support diverse asset types including LSTs, stablecoins, and protocol tokens
- Validate data integrity at each step
Validation
Freshness
| Operation type | Max age |
|---|
| User operations (supply, borrow, repay, withdraw) | 600 seconds (10 minutes) |
| Liquidations | 7200 seconds (2 hours) |
Pyth and Redstone use Unix timestamp freshness. Chainlink uses slot-based freshness (≈400 ms per slot).
Confidence (Pyth only)
Pyth feeds include a confidence interval. The oracle rejects feeds whose confidence exceeds:
| Operation type | Max confidence |
|---|
| User operations | 2% of price |
| Liquidations | 4% of price |
Only Pyth validates confidence. Other sources (Chainlink, Redstone, StakePool, MsolPool, SinglePool, JupLend) either derive rates on-chain or only enforce freshness; they do not expose or check a confidence interval.
Providers
| Provider | Source | Use case |
|---|
| Pyth | Pyth | Spot prices (SOL, ETH, BTC, JLP, etc.) |
| Chainlink | Chainlink | SOL/USD, WBTC/USD, EURC/USD |
| Redstone | Redstone | Additional price feeds |
| Marinade | MsolPool | mSOL/SOL |
| Jito | StakePool | JITOSOL/SOL |
| SPL Single Pool | SinglePool | Staked SOL (Helius, Jupiter, Nansen, Emerald, Shift, Kiln) |
| JupLend | JupLend | fToken exchange rates (e.g., JUPUSD) |
Oracle Verification Script
Use this script to verify oracle prices by providing a nonce. The oracle PDA is derived from the oracle seed and a 16-bit nonce.
For oracles that use SinglePool or JupLend sources, remainingAccounts must include 3 or 4 consecutive source accounts per hop respectively, as configured in the oracle. The script below works for simple oracles (e.g., single Pyth or Chainlink feed).
import { Program, AnchorProvider } from "@coral-xyz/anchor";
import { Connection, PublicKey } from "@solana/web3.js";
import axios from "axios";
const NONCE = 2; // Change this to test different oracles
const ORACLE_PROGRAM_ID = new PublicKey("jupnw4B6Eqs7ft6rxpzYLJZYSnrpRgPcr589n5Kv4oc");
const IDL_URL = "https://raw.githubusercontent.com/jup-ag/jupiter-lend/refs/heads/main/target/idl/oracle.json";
const RPC_URL = "<RPC URL>";
class OracleReader {
private program: Program;
private constructor(program: Program) {
this.program = program;
}
static async create(): Promise<OracleReader> {
const connection = new Connection(RPC_URL);
const response = await axios.get(IDL_URL);
const idl = response.data;
const wallet = {
signTransaction: () => { throw new Error("Read-only"); },
signAllTransactions: () => { throw new Error("Read-only"); },
publicKey: new PublicKey("11111111111111111111111111111111"),
};
const provider = new AnchorProvider(connection, wallet, {});
const program = new Program(idl, provider);
return new OracleReader(program);
}
private findOraclePDA(nonce: number): PublicKey {
const [pda] = PublicKey.findProgramAddressSync(
[
Buffer.from("oracle"),
Buffer.from(new Uint8Array(new Uint16Array([nonce]).buffer)),
],
ORACLE_PROGRAM_ID
);
return pda;
}
async getPrice(nonce: number) {
const oraclePDA = this.findOraclePDA(nonce);
const oracleAccount = await this.program.account.oracle.fetch(oraclePDA);
const remainingAccounts = oracleAccount.sources.map((source: any) => ({
pubkey: source.source,
isWritable: false,
isSigner: false,
}));
const price = await this.program.methods
.getExchangeRateOperate(nonce)
.accounts({ oracle: oraclePDA })
.remainingAccounts(remainingAccounts)
.view();
return {
nonce,
oraclePDA: oraclePDA.toString(),
price: price.toString(),
sources: oracleAccount.sources.length
};
}
}
async function main() {
const reader = await OracleReader.create();
const result = await reader.getPrice(NONCE);
console.log(result);
}
main().catch(console.error);