Skip to content

WIP: Oracle with Kamino Scope Aggregator#73

Open
santy311 wants to merge 43 commits intomainfrom
santhosh/oracle-limit-agg
Open

WIP: Oracle with Kamino Scope Aggregator#73
santy311 wants to merge 43 commits intomainfrom
santhosh/oracle-limit-agg

Conversation

@santy311
Copy link
Collaborator

@santy311 santy311 commented Jul 5, 2025

Scope is a price oracle aggregator. It's one of three oracle options alongside Pyth and Switchboard. It has an account which can support around 500 token price pair.

The major problem is that it does not have on-chain mapping. It is diabolical as they have so many oracle prices but we need to maintain the mapping somehow on chain.

So, looking into how Klend solves this, there is something call the price chain system

  • Each token uses a price_chain: [u16; 4] array of token IDs
  • Example: [1, 2, 0, 0] means Token → USDC → USD conversion
  • Token IDs are indices into the Scope price account's price array
pub struct ScopeConfiguration {
    pub price_feed: Pubkey,        // Scope price account address
    pub price_chain: [u16; 4],     // Token ID conversion chain
    ...
}

here Kamino does a clever way of getting the price of a token through hops, the price chain contains the mapping of the token.

For example there is some shitCoin and the oracle exists for shitCoin/Sol in the scope, then price_chain will be something like this

[32, 0, u16::MAX, u16::MAX]

The 32 is the index for shitCoin/Sol price in the scope account. Now, the second index is 0 which will give the index Sol/USD. skip for the next ones.

The problem is for each reserve in the lending, a separate config is maintained. In short:

  • Each reserve has its own ScopeConfiguration
  • Mapping is set administratively via update_reserve_config
  • No global mint-to-tokenID mapping exists

Whenever a new reserve is created, some admin guy from Kamino calls the update_reserve instruction to set the price feed account

Now, for the oracle we need not keep track of multiple acounts but we need to keep track of the index.

The Problem

There is no on-chain mapping from mint addresses to Scope token index. Also we need to get the decimal of the mint as we only have the ATAs which does not contain the decimal info.

The reason why we need decimals, for example I buy 100 Trump coin but the swig knows 1 trump coin is 8 dollars. But in the program we recv something like 10_000_000, but we cannot get the price of the 100 Trump without the decimal of Trump.

I'll update the proposed solution section to address both the mapping and decimal problems comprehensively.

Proposed Solutions

We can create an on-chain mapping system (separate program) that handles both the mint-to-index mapping and decimal information. As using Kamino’s reserve for our use case to fetch the mapping will be very messy and we need to have better control over it.

Design

pub struct ScopeMappingRegistry {
    pub authority: Pubkey,
    pub mappings: Vec<MintMapping>,
    pub version: u8,
    pub total_mappings: u32,
}

pub struct MintMapping {
    pub mint: Pubkey,
    pub token_id: u16,           // Scope price account index
    pub price_chain: [u16; 4],   // Conversion chain (e.g., [32, 0, u16::MAX, u16::MAX])
    pub decimals: u8,            // Mint decimals for price calculations
    pub is_active: bool,
    pub last_updated: i64,
    pub pyth_account: Pubkey,    // can also be used
    pub switch_board: Pubkey,    // can also be used
}

// Helper struct for efficient lookups
pub struct MintLookup {
    pub mint: Pubkey,
    pub mapping_index: u32,      // Index into the mappings vector
}

Also we have to add, so that when a new oracle price feed is added, we can add it to our mapping.

pub fn add_mint_mapping(
    ctx: Context<AddMintMapping>,
    mint: Pubkey,
    token_id: u16,
    price_chain: [u16; 4],
    decimals: u8,
) -> Result<()> {
    // Add new mapping to registry
}

pub fn update_mint_mapping(
    ctx: Context<UpdateMintMapping>,
    mint: Pubkey,
    new_token_id: u16,
    new_price_chain: [u16; 4],
) -> Result<()> {
    // Update existing mapping
}

@santy311 santy311 force-pushed the santhosh/oracle-limit-agg branch from d026a6a to 0c77ff0 Compare July 5, 2025 22:34
@tracy-codes
Copy link
Contributor

@santy311 I think moving forward with an on-chain mapping system for mint-to-index conversion and getting decimal info is a good idea.

Would this be an added instruction that's called for this case, if it needs to call out to an external program to get the index for the mint?

@santy311
Copy link
Collaborator Author

santy311 commented Jul 9, 2025

Would this be an added instruction that's called for this case, if it needs to call out to an external program to get the index for the mint?

@tracy-codes I was thinking of maintaining a separate program that will take care of the mapping. The oracle prices also contains tokens which are not required for our case like, ETH/USD and so on. So we have better control over the tokens needed.

So answering to the question, we will be passing just two accounts, Scope which has the oracle data and the registry account (maintained by us). I do not think we need an added ix.

@santy311 santy311 changed the title Oracle with Kamino Scope Aggregator WIP: Oracle with Kamino Scope Aggregator Jul 16, 2025
@santy311
Copy link
Collaborator Author

santy311 commented Jul 16, 2025

@tracy-codes moving this to WIP unless until the scope mapping program has been deployed

Copy link
Contributor

@tracy-codes tracy-codes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of general questions / comments:

  1. Can we implement oracle price staleness checks? This way we can reject if the oracle price for an asset is stale as it's no longer trustworthy
  2. Is there a need for a test case that covers if there's multiple oracle limits? Just want to ensure if there's multiple, they don't cause any weird effects.
  3. This branch needs to be updated against main / merge conflicts resolved please.

@santy311
Copy link
Collaborator Author

santy311 commented Aug 4, 2025

Regarding

  1. I have added a staleness check before the price is read. Right now, I kept it around 60 seconds. The price in scope is updated every 15 to 20 seconds. I have added enough headroom for the slot diff between price update and swig ix execution. I have also added a test case for the stale check. Since the price data is from mainnet, the litesvm will sync with the mainnet slot.

  2. I created a report to check the price diff between different oracles, the price from kamino scope and pyth is identical. The switchboard oracle price is maintained by unknown 3rd party (not sponsored feed like in Pyth). The current impl reads the value only from the scope but I designed it in such a way that we can fetch the price from oracle we want but then pyth and switchboard comes under the expense of sending an additional account in the instruction. Right now, with stale check, we are good to go with just scope.

  3. Fixed the merge conflicts for the PR. Have to fix the github action for using private repo for oracle mapper crate

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants