├── sdk ├── src │ ├── lib.rs │ ├── constants.rs │ ├── transaction.rs │ └── accounts.rs └── Cargo.toml ├── examples ├── keys │ └── admin-keypair.json ├── accounts │ ├── admin-account.json │ ├── sol-usdc-price-feed-oracle.json │ ├── bonk-sol-price-feed-oracle.json │ └── sol-usdt-price-feed-oracle.json ├── src │ ├── fetch.rs │ ├── constants.rs │ ├── single_price_feed.rs │ └── multiple_price_feed.rs └── Cargo.toml ├── .gitignore ├── program ├── benches │ ├── compute_units.md │ └── compute_units.rs ├── src │ └── lib.rs ├── Cargo.toml └── tests │ └── tests.rs ├── SECURITY.md ├── .taplo.toml ├── doppler ├── Cargo.toml └── src │ ├── panic_handler.rs │ ├── lib.rs │ ├── oracle.rs │ └── admin.rs ├── test-validator.sh ├── LICENSE ├── Cargo.toml ├── assets └── logo.svg └── README.md /sdk/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod accounts; 2 | mod constants; 3 | pub mod transaction; 4 | pub use accounts::{Oracle, UpdateInstruction}; 5 | pub use constants::ID; 6 | -------------------------------------------------------------------------------- /examples/keys/admin-keypair.json: -------------------------------------------------------------------------------- 1 | [25,228,1,242,18,100,139,138,81,202,81,20,91,147,145,23,253,181,64,125,38,159,134,38,125,145,128,188,180,214,188,105,8,157,190,201,100,151,171,208,219,33,121,82,105,186,185,75,200,184,73,204,5,170,148,84,208,165,220,118,236,203,81,209] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust build artifacts 2 | /target 3 | /sdk/target 4 | /program/target 5 | /deploy 6 | 7 | # Configuration files 8 | .DS_Store 9 | 10 | # testing 11 | test-ledger 12 | 13 | # Lock file (optional - you may want to commit this for reproducible builds) 14 | Cargo.lock -------------------------------------------------------------------------------- /program/benches/compute_units.md: -------------------------------------------------------------------------------- 1 | #### 2025-09-03 17:37:21.760974372 UTC 2 | 3 | Solana CLI Version: solana-cli 2.1.21 (src:8a085eeb; feat:1416569292, client:Agave) 4 | 5 | | Name | CUs | Delta | 6 | | --------------- | --- | ------- | 7 | | CreatePriceFeed | 150 | - new - | 8 | | PriceFeedUpdate | 21 | - new - | 9 | -------------------------------------------------------------------------------- /examples/accounts/admin-account.json: -------------------------------------------------------------------------------- 1 | { 2 | "pubkey": "admnz5UvRa93HM5nTrxXmsJ1rw2tvXMBFGauvCgzQhE", 3 | "account": { 4 | "lamports": 1000000000, 5 | "data": ["", "base64"], 6 | "owner": "11111111111111111111111111111111", 7 | "executable": false, 8 | "rentEpoch": 1000000000, 9 | "space": 0 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | We are highly responsive to security reports and verified production issues. 4 | 5 | `doppler` is currently in an un-audited state and considered pre-production. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Please contact us on our discord at [discord.gg/blueshift](https://discord.gg/blueshift) 10 | -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | [formatting] 2 | column_width = 120 3 | array_auto_expand = true 4 | allowed_blank_lines = 1 5 | 6 | [[rule]] 7 | include = ["**/Cargo.toml"] 8 | keys = [ 9 | "dependencies", 10 | "dev-dependencies", 11 | "build-dependencies", 12 | "patch.crates-io", 13 | "toolchain", 14 | "workspace.dependencies", 15 | ] 16 | 17 | [rule.formatting] 18 | reorder_keys = true -------------------------------------------------------------------------------- /examples/src/fetch.rs: -------------------------------------------------------------------------------- 1 | use doppler_sdk::Oracle; 2 | use solana_client::rpc_client::RpcClient; 3 | use solana_pubkey::Pubkey; 4 | 5 | pub fn oracle_account( 6 | client: &RpcClient, 7 | oracle_pubkey: &Pubkey, 8 | ) -> Option> { 9 | client 10 | .get_account_data(oracle_pubkey) 11 | .ok() 12 | .map(|b| Oracle::::from_bytes(b.as_slice())) 13 | } 14 | -------------------------------------------------------------------------------- /examples/accounts/sol-usdc-price-feed-oracle.json: -------------------------------------------------------------------------------- 1 | { 2 | "pubkey": "QUVF91dzXWYvE5FmFEc41JZxRDmNgx8S8P6sNDWYZiW", 3 | "account": { 4 | "lamports": 1002240, 5 | "data": [ 6 | "AAAAAAAAAACghgEAAAAAAA==", 7 | "base64" 8 | ], 9 | "owner": "fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm", 10 | "executable": false, 11 | "rentEpoch": 1000000000, 12 | "space": 16 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/accounts/bonk-sol-price-feed-oracle.json: -------------------------------------------------------------------------------- 1 | { 2 | "pubkey": "6uQ848roY5vumz43QeQguE7xCyBSmgZbwNdJMTrs2Xhy", 3 | "account": { 4 | "lamports": 1002240, 5 | "data": [ 6 | "AAAAAAAAAACghgEAAAAAAA==", 7 | "base64" 8 | ], 9 | "owner": "fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm", 10 | "executable": false, 11 | "rentEpoch": 1000000000, 12 | "space": 16 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/accounts/sol-usdt-price-feed-oracle.json: -------------------------------------------------------------------------------- 1 | { 2 | "pubkey": "9bA7GPqPpZ5aLbwb8E6cKvUPM8pcHXXTqLpf5zLAqHP5", 3 | "account": { 4 | "lamports": 1002240, 5 | "data": [ 6 | "AAAAAAAAAACghgEAAAAAAA==", 7 | "base64" 8 | ], 9 | "owner": "fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm", 10 | "executable": false, 11 | "rentEpoch": 1000000000, 12 | "space": 16 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/src/constants.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use solana_pubkey::Pubkey; 4 | 5 | pub const SOL_USDC_ORACLE: Pubkey = 6 | Pubkey::from_str_const("QUVF91dzXWYvE5FmFEc41JZxRDmNgx8S8P6sNDWYZiW"); 7 | pub const SOL_USDT_ORACLE: Pubkey = 8 | Pubkey::from_str_const("9bA7GPqPpZ5aLbwb8E6cKvUPM8pcHXXTqLpf5zLAqHP5"); 9 | pub const BONK_SOL_ORACLE: Pubkey = 10 | Pubkey::from_str_const("6uQ848roY5vumz43QeQguE7xCyBSmgZbwNdJMTrs2Xhy"); 11 | -------------------------------------------------------------------------------- /doppler/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "doppler" 3 | description = "Doppler building blocks." 4 | repository = { workspace = true } 5 | readme = { workspace = true } 6 | license-file = { workspace = true } 7 | edition = { workspace = true } 8 | version = { workspace = true } 9 | 10 | [features] 11 | default = [] 12 | std = [] 13 | 14 | [lints.rust] 15 | unexpected_cfgs = { level = "warn", check-cfg = [ 16 | 'cfg(target_os, values("solana"))', 17 | ] } 18 | -------------------------------------------------------------------------------- /test-validator.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | solana-test-validator \ 6 | --bpf-program fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm ./target/deploy/doppler_program.so \ 7 | --account QUVF91dzXWYvE5FmFEc41JZxRDmNgx8S8P6sNDWYZiW ./examples/accounts/sol-usdc-price-feed-oracle.json \ 8 | --account 9bA7GPqPpZ5aLbwb8E6cKvUPM8pcHXXTqLpf5zLAqHP5 ./examples/accounts/sol-usdt-price-feed-oracle.json \ 9 | --account 6uQ848roY5vumz43QeQguE7xCyBSmgZbwNdJMTrs2Xhy ./examples/accounts/bonk-sol-price-feed-oracle.json \ 10 | --account admnz5UvRa93HM5nTrxXmsJ1rw2tvXMBFGauvCgzQhE ./examples/accounts/admin-account.json -r -------------------------------------------------------------------------------- /program/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![cfg_attr(target_os = "solana", feature(asm_experimental_arch))] 3 | 4 | // fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm 5 | use doppler::{nostd_panic_handler, prelude::*}; 6 | 7 | #[repr(C)] 8 | #[derive(Clone, Copy)] 9 | pub struct PriceFeed { 10 | pub price: u64, 11 | } 12 | 13 | nostd_panic_handler!(); 14 | 15 | #[no_mangle] 16 | /// # Safety 17 | /// 18 | /// This is a permissioned entrypoint only invokable by the 19 | /// ADMIN keypair. It is as safe as you choose it to be. 20 | pub unsafe extern "C" fn entrypoint(input: *mut u8) { 21 | Admin::check(input); 22 | Oracle::::check_and_update(input); 23 | } 24 | -------------------------------------------------------------------------------- /sdk/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "doppler-sdk" 3 | description = "An ultra-optimized oracle SDK." 4 | repository = { workspace = true } 5 | readme = { workspace = true } 6 | license-file = { workspace = true } 7 | edition = { workspace = true } 8 | version = { workspace = true } 9 | 10 | [dependencies] 11 | solana-compute-budget-interface = { workspace = true } 12 | solana-hash = { workspace = true } 13 | solana-instruction = { workspace = true } 14 | solana-keypair = { workspace = true } 15 | solana-pubkey = { workspace = true } 16 | solana-signer = { workspace = true } 17 | solana-transaction = { workspace = true, features = ["bincode"] } 18 | 19 | [dev-dependencies] 20 | doppler-program = { workspace = true } 21 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "doppler-examples" 3 | description = "Examples on how to use Doppler." 4 | repository = { workspace = true } 5 | readme = { workspace = true } 6 | license-file = { workspace = true } 7 | edition = { workspace = true } 8 | version = { workspace = true } 9 | 10 | [dependencies] 11 | doppler-program = { workspace = true } 12 | doppler-sdk = { workspace = true } 13 | solana-client = { workspace = true } 14 | solana-keypair = { workspace = true } 15 | solana-pubkey = { workspace = true } 16 | solana-signer = { workspace = true } 17 | 18 | [[bin]] 19 | name = "single-price-feed" 20 | path = "src/single_price_feed.rs" 21 | 22 | [[bin]] 23 | name = "multiple-price-feed" 24 | path = "src/multiple_price_feed.rs" 25 | -------------------------------------------------------------------------------- /sdk/src/constants.rs: -------------------------------------------------------------------------------- 1 | use solana_pubkey::Pubkey; 2 | 3 | // fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm 4 | pub const ID: Pubkey = Pubkey::new_from_array([ 5 | 0x09, 0xe2, 0x60, 0x40, 0xff, 0x10, 0xec, 0xcf, 0xc1, 0x6a, 0xf6, 0x16, 0x9a, 0x68, 0x04, 0x78, 6 | 0x15, 0x14, 0x33, 0x02, 0xac, 0x6e, 0x98, 0x5f, 0x70, 0x85, 0x53, 0xe1, 0x0a, 0xb6, 0xf9, 0x22, 7 | ]); 8 | 9 | pub(crate) const SEQUENCE_CHECK_CU: u32 = 5; 10 | pub(crate) const ADMIN_VERIFICATION_CU: u32 = 6; 11 | pub(crate) const PAYLOAD_WRITE_CU: u32 = 6; 12 | 13 | pub(crate) const COMPUTE_BUDGET_IX_CU: u32 = 150; 14 | pub(crate) const COMPUTE_BUDGET_UNIT_PRICE_SIZE: u32 = 9; 15 | pub(crate) const COMPUTE_BUDGET_UNIT_LIMIT_SIZE: u32 = 5; 16 | pub(crate) const COMPUTE_BUDGET_DATA_LIMIT_SIZE: u32 = 5; 17 | pub(crate) const COMPUTE_BUDGET_PROGRAM_SIZE: u32 = 22; 18 | pub(crate) const ORACLE_PROGRAM_SIZE: u32 = 36; 19 | -------------------------------------------------------------------------------- /program/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "doppler-program" 3 | description = "An ultra-optimized oracle program for Solana." 4 | repository = { workspace = true } 5 | readme = { workspace = true } 6 | license-file = { workspace = true } 7 | edition = { workspace = true } 8 | version = { workspace = true } 9 | 10 | [lib] 11 | crate-type = ["lib", "cdylib"] 12 | 13 | [lints.rust] 14 | unexpected_cfgs = { level = "warn", check-cfg = [ 15 | 'cfg(target_os, values("solana"))', 16 | ] } 17 | 18 | [dependencies] 19 | doppler = { workspace = true } 20 | 21 | [dev-dependencies] 22 | doppler-sdk = { workspace = true } 23 | mollusk-svm = { workspace = true } 24 | mollusk-svm-bencher = { workspace = true } 25 | solana-account = { workspace = true } 26 | solana-clock = { workspace = true } 27 | solana-instruction = { workspace = true } 28 | solana-pubkey = { workspace = true } 29 | solana-sdk-ids = { workspace = true } 30 | solana-system-interface = { workspace = true, features = ["bincode"] } 31 | 32 | [[bench]] 33 | name = "compute_units" 34 | harness = false 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Blueshift Labs Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["doppler", "examples", "program", "sdk"] 4 | 5 | [workspace.package] 6 | repository = "https://github.com/blueshift-gg/doppler" 7 | readme = "README.md" 8 | license-file = "LICENSE" 9 | edition = "2021" 10 | version = "0.1.0" 11 | 12 | [workspace.dependencies] 13 | doppler = { path = "./doppler" } 14 | doppler-program = { path = "./program" } 15 | doppler-sdk = { path = "./sdk" } 16 | mollusk-svm = { version = "0.5.1" } 17 | mollusk-svm-bencher = { version = "0.5.1" } 18 | serde = { version = "1.0.219" } 19 | solana-account = { version = "2.2.1" } 20 | solana-client = { version = "2.2.3" } 21 | solana-clock = { version = "2.2.2" } 22 | solana-compute-budget-interface = { version = "2.2.2" } 23 | solana-hash = { version = "2.2.1" } 24 | solana-instruction = { version = "2.3.0" } 25 | solana-keypair = { version = "2.2.3" } 26 | solana-program = { version = "2.3.0" } 27 | solana-pubkey = { version = "2.3.0" } 28 | solana-sdk-ids = { version = "2.2.1" } 29 | solana-signer = { version = "2.2.1" } 30 | solana-system-interface = { version = "1.0.0" } 31 | solana-transaction = { version = "2.2.3" } 32 | 33 | [profile.release] 34 | opt-level = 3 35 | lto = true 36 | codegen-units = 1 37 | -------------------------------------------------------------------------------- /doppler/src/panic_handler.rs: -------------------------------------------------------------------------------- 1 | #[allow(dead_code)] 2 | extern "C" { 3 | pub fn sol_panic_(filename: *const u8, filename_len: u64, line: u64, column: u64) -> !; 4 | } 5 | 6 | #[macro_export] 7 | macro_rules! nostd_panic_handler { 8 | () => { 9 | /// A panic handler for `no_std`. 10 | #[cfg(target_os = "solana")] 11 | #[no_mangle] 12 | #[panic_handler] 13 | pub fn panic_handler(info: &core::panic::PanicInfo<'_>) -> ! { 14 | if let Some(location) = info.location() { 15 | unsafe { 16 | $crate::panic_handler::sol_panic_( 17 | location.file().as_ptr(), 18 | location.file().len() as u64, 19 | location.line() as u64, 20 | location.column() as u64, 21 | ) 22 | } 23 | } else { 24 | // If no location info, just abort 25 | unsafe { core::arch::asm!("abort", options(noreturn)) } 26 | } 27 | } 28 | 29 | #[cfg(not(target_os = "solana"))] 30 | mod __private_panic_handler { 31 | extern crate std as __std; 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /doppler/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(target_os = "solana", feature(asm_experimental_arch))] 2 | #![cfg_attr(not(feature = "std"), no_std)] 3 | 4 | mod admin; 5 | mod oracle; 6 | pub mod panic_handler; 7 | 8 | /// Helper to read a value at offset and cast it 9 | /// 10 | /// # Safety 11 | /// - The caller must ensure that `ptr.add(offset)` is a valid pointer and properly aligned for type `T`. 12 | /// - The memory at the computed address must be initialized and valid for reads of type `T`. 13 | #[inline(always)] 14 | const unsafe fn read(ptr: *const u8, offset: usize) -> T 15 | where 16 | T: core::marker::Copy, 17 | { 18 | *ptr.add(offset).cast::() 19 | } 20 | 21 | /// Helper to write a value at offset 22 | /// 23 | /// # Safety 24 | /// - The caller must ensure that `ptr.add(offset)` is a valid pointer and properly aligned for type `T`. 25 | /// - The memory at the computed address must be valid for writes of type `T`. 26 | #[inline(always)] 27 | unsafe fn write(ptr: *mut u8, offset: usize, value: T) 28 | where 29 | T: core::marker::Copy, 30 | { 31 | *ptr.add(offset).cast::() = value; 32 | } 33 | 34 | pub mod prelude { 35 | pub use crate::admin::{Admin, ADMIN}; 36 | pub use crate::oracle::Oracle; 37 | #[cfg(not(feature = "std"))] 38 | pub use crate::panic_handler::*; 39 | } 40 | -------------------------------------------------------------------------------- /doppler/src/oracle.rs: -------------------------------------------------------------------------------- 1 | // Account data offsets 2 | const ORACLE_SEQUENCE: usize = 0x28c0; // (sequence: u64) 3 | const ORACLE_PAYLOAD: usize = 0x28c8; // (payload: T) 4 | 5 | #[repr(C)] 6 | pub struct Oracle { 7 | sequence: u64, // timestamp_millis, timestamp_seconds, autoincrement, whatever 8 | payload: T, 9 | } 10 | 11 | impl Oracle { 12 | // Relative offsets for instruction data 13 | const INSTRUCTION_SEQUENCE: usize = 0x50d8 + core::mem::size_of::(); // (sequence: u64) 14 | const INSTRUCTION_PAYLOAD: usize = 0x50e0 + core::mem::size_of::(); // (payload: T) 15 | 16 | /// # Safety 17 | /// 18 | /// The caller must ensure that `ptr` is a valid pointer to a memory region 19 | /// that is properly aligned and large enough to hold the data being read or written. 20 | /// Additionally, the memory region must not be accessed concurrently by other threads. 21 | #[inline(always)] 22 | pub unsafe fn check_and_update(ptr: *mut u8) { 23 | // Check timestamp validity 24 | let current_sequence = crate::read::(ptr, ORACLE_SEQUENCE); 25 | let new_sequence = crate::read::(ptr, Self::INSTRUCTION_SEQUENCE); 26 | 27 | if new_sequence <= current_sequence { 28 | #[cfg(target_os = "solana")] 29 | unsafe { 30 | core::arch::asm!("lddw r0, 2\nexit"); 31 | } 32 | } 33 | 34 | // Update oracle data 35 | let new_payload = crate::read::(ptr, Self::INSTRUCTION_PAYLOAD); 36 | crate::write(ptr, ORACLE_SEQUENCE, new_sequence); 37 | crate::write(ptr, ORACLE_PAYLOAD, new_payload); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /doppler/src/admin.rs: -------------------------------------------------------------------------------- 1 | const ADMIN_HEADER: usize = 0x0008; 2 | const ADMIN_KEY: usize = 0x0010; 3 | 4 | // admnz5UvRa93HM5nTrxXmsJ1rw2tvXMBFGauvCgzQhE 5 | pub const ADMIN: [u8; 32] = [ 6 | 0x08, 0x9d, 0xbe, 0xc9, 0x64, 0x97, 0xab, 0xd0, 0xdb, 0x21, 0x79, 0x52, 0x69, 0xba, 0xb9, 0x4b, 7 | 0xc8, 0xb8, 0x49, 0xcc, 0x05, 0xaa, 0x94, 0x54, 0xd0, 0xa5, 0xdc, 0x76, 0xec, 0xcb, 0x51, 0xd1, 8 | ]; 9 | 10 | // Account flags 11 | pub const NO_DUP_SIGNER: u16 = 0x01 << 8 | 0xff; // SIGNER | NO_DUP 12 | 13 | pub struct Admin; 14 | 15 | impl Admin { 16 | #[inline(always)] 17 | /// # Check 18 | /// Performs the following checks on the Admin account: 19 | /// - Checks Admin is a non-duplicate signer (2 CUs) 20 | /// - Checks Admin address matches ADMIN (12 CUs) 21 | /// 22 | /// # Safety 23 | /// - The caller must ensure that `ptr` is a valid pointer to a memory region 24 | /// that can be safely read from. 25 | /// - The memory region must be properly aligned and large enough to hold the 26 | /// data being read. 27 | pub unsafe fn check(ptr: *mut u8) { 28 | if crate::read::(ptr, ADMIN_HEADER) != NO_DUP_SIGNER 29 | || crate::read::(ptr, ADMIN_KEY) != *ADMIN.as_ptr().cast::() 30 | || crate::read::(ptr, ADMIN_KEY + 0x08) != *ADMIN.as_ptr().add(8).cast::() 31 | || crate::read::(ptr, ADMIN_KEY + 0x10) != *ADMIN.as_ptr().add(16).cast::() 32 | || crate::read::(ptr, ADMIN_KEY + 0x18) != *ADMIN.as_ptr().add(24).cast::() 33 | { 34 | #[cfg(target_os = "solana")] 35 | unsafe { 36 | core::arch::asm!("lddw r0, 1\nexit"); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/src/single_price_feed.rs: -------------------------------------------------------------------------------- 1 | use doppler_program::PriceFeed; 2 | use doppler_sdk::{transaction::Builder, Oracle}; 3 | use solana_client::rpc_client::RpcClient; 4 | use solana_keypair::Keypair; 5 | use solana_signer::EncodableKey as _; 6 | use std::path::PathBuf; 7 | 8 | mod constants; 9 | mod fetch; 10 | 11 | fn main() { 12 | // Connect to local Solana cluster 13 | let rpc_url = "http://localhost:8899"; 14 | let client = RpcClient::new(rpc_url.to_string()); 15 | 16 | let keypair_path: PathBuf = [env!("CARGO_MANIFEST_DIR"), "keys", "admin-keypair.json"] 17 | .iter() 18 | .collect(); 19 | 20 | // Load admin keypair (ensure this path is correct) 21 | let admin = Keypair::read_from_file(keypair_path).expect("keypair not found at that path"); 22 | 23 | // Define oracle account public key (replace with actual oracle account) 24 | let oracle_data = fetch::oracle_account::(&client, &constants::SOL_USDC_ORACLE) 25 | .expect("failed to fetch oracle account"); 26 | 27 | // Create the new price feed data 28 | let new_price_feed = PriceFeed { 29 | price: oracle_data.payload.price + 10, 30 | }; 31 | 32 | // Get a recent blockhash 33 | let recent_blockhash = client 34 | .get_latest_blockhash() 35 | .expect("Failed to get recent blockhash"); 36 | 37 | // Create and sign the transaction 38 | let transaction = Builder::new(&admin) 39 | .add_oracle_update( 40 | constants::SOL_USDC_ORACLE, 41 | Oracle { 42 | sequence: oracle_data.sequence + 1, // New sequence number, must be greater than current 43 | payload: new_price_feed, 44 | }, 45 | ) 46 | .with_unit_price(1_000) 47 | .build(recent_blockhash); 48 | 49 | println!("Sending Tx..."); 50 | 51 | // Send the transaction 52 | let signature = client 53 | .send_and_confirm_transaction(&transaction) 54 | .expect("Failed to send transaction"); 55 | 56 | println!("Transaction successful with signature: {signature:?}"); 57 | 58 | let oracle_data = fetch::oracle_account::(&client, &constants::SOL_USDC_ORACLE) 59 | .expect("failed to fetch oracle account"); 60 | 61 | println!( 62 | "Price feed : seq : {}, price : {}", 63 | oracle_data.sequence, oracle_data.payload.price 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /sdk/src/transaction.rs: -------------------------------------------------------------------------------- 1 | use solana_compute_budget_interface::ComputeBudgetInstruction; 2 | use solana_hash::Hash; 3 | use solana_instruction::Instruction; 4 | use solana_keypair::Keypair; 5 | use solana_pubkey::Pubkey; 6 | use solana_signer::Signer as _; 7 | use solana_transaction::Transaction; 8 | 9 | use crate::accounts::{Oracle, UpdateInstruction}; 10 | use crate::constants::{ 11 | COMPUTE_BUDGET_DATA_LIMIT_SIZE, COMPUTE_BUDGET_IX_CU, COMPUTE_BUDGET_PROGRAM_SIZE, 12 | COMPUTE_BUDGET_UNIT_LIMIT_SIZE, COMPUTE_BUDGET_UNIT_PRICE_SIZE, ORACLE_PROGRAM_SIZE, 13 | }; 14 | 15 | pub struct Builder<'a> { 16 | oracle_update_ixs: Vec, 17 | admin: &'a Keypair, 18 | unit_price: Option, 19 | compute_units: u32, 20 | loaded_account_data_size: u32, 21 | } 22 | 23 | impl<'a> Builder<'a> { 24 | #[must_use] 25 | pub const fn new(admin: &'a Keypair) -> Self { 26 | Self { 27 | admin, 28 | oracle_update_ixs: vec![], 29 | unit_price: None, 30 | compute_units: COMPUTE_BUDGET_IX_CU * 2, // default 2 compute budget ixs 31 | loaded_account_data_size: ORACLE_PROGRAM_SIZE 32 | + COMPUTE_BUDGET_PROGRAM_SIZE 33 | + COMPUTE_BUDGET_UNIT_LIMIT_SIZE 34 | + COMPUTE_BUDGET_DATA_LIMIT_SIZE 35 | + 2, 36 | } 37 | } 38 | 39 | pub fn add_oracle_update( 40 | mut self, 41 | oracle_pubkey: Pubkey, 42 | oracle: Oracle, 43 | ) -> Self { 44 | let update_ix = UpdateInstruction { 45 | admin: self.admin.pubkey(), 46 | oracle_pubkey, 47 | oracle, 48 | }; 49 | 50 | self.compute_units += update_ix.compute_units(); 51 | self.loaded_account_data_size += update_ix.loaded_accounts_data_size_limit() * 2; 52 | 53 | self.oracle_update_ixs.push(update_ix.into()); 54 | 55 | self 56 | } 57 | 58 | #[must_use] 59 | pub const fn with_unit_price(mut self, micro_lamports: u64) -> Self { 60 | self.unit_price = Some(micro_lamports); 61 | self 62 | } 63 | 64 | #[must_use] 65 | pub fn build(self, recent_blockhash: Hash) -> Transaction { 66 | let mut ixs = Vec::with_capacity(self.oracle_update_ixs.len() + 3); 67 | let mut loaded_account_data_size = self.loaded_account_data_size; 68 | let mut compute_units = self.compute_units; 69 | 70 | if let Some(unit_price) = self.unit_price { 71 | ixs.push(ComputeBudgetInstruction::set_compute_unit_price(unit_price)); 72 | loaded_account_data_size += COMPUTE_BUDGET_UNIT_PRICE_SIZE; 73 | compute_units += COMPUTE_BUDGET_IX_CU; 74 | } 75 | 76 | ixs.push( 77 | ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(loaded_account_data_size), 78 | ); 79 | ixs.push(ComputeBudgetInstruction::set_compute_unit_limit( 80 | compute_units, 81 | )); 82 | 83 | for oracle_ix in self.oracle_update_ixs { 84 | ixs.push(oracle_ix); 85 | } 86 | 87 | Transaction::new_signed_with_payer( 88 | &ixs, 89 | Some(&self.admin.pubkey()), 90 | &[&self.admin], 91 | recent_blockhash, 92 | ) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /program/benches/compute_units.rs: -------------------------------------------------------------------------------- 1 | use doppler::prelude::*; 2 | use doppler_program::PriceFeed; 3 | use doppler_sdk::{Oracle, UpdateInstruction}; 4 | use mollusk_svm::{program::keyed_account_for_system_program, Mollusk}; 5 | use mollusk_svm_bencher::MolluskComputeUnitBencher; 6 | use solana_account::Account; 7 | use solana_clock::Epoch; 8 | use solana_instruction::Instruction; 9 | use solana_pubkey::Pubkey; 10 | 11 | #[must_use] 12 | pub fn keyed_account_for_admin(key: Pubkey) -> (Pubkey, Account) { 13 | ( 14 | key, 15 | Account::new(10_000_000_000, 0, &solana_sdk_ids::system_program::ID), 16 | ) 17 | } 18 | 19 | pub fn keyed_account_for_oracle( 20 | mollusk: &mut Mollusk, 21 | admin: Pubkey, 22 | seed: &str, 23 | payload: T, 24 | ) -> (Pubkey, Account) { 25 | let oracle_account = Oracle { 26 | sequence: 0, 27 | payload, 28 | }; 29 | 30 | let key = Pubkey::create_with_seed(&admin, seed, &doppler_sdk::ID).unwrap(); 31 | 32 | let lamports = mollusk 33 | .sysvars 34 | .rent 35 | .minimum_balance(core::mem::size_of::>()); 36 | 37 | let data = oracle_account.to_bytes(); 38 | 39 | let account = Account { 40 | lamports, 41 | data, 42 | owner: doppler_sdk::ID, 43 | executable: false, 44 | rent_epoch: Epoch::default(), 45 | }; 46 | 47 | (key, account) 48 | } 49 | 50 | fn main() { 51 | // Create Mollusk instance 52 | let mut mollusk = Mollusk::new(&doppler_sdk::ID, "../target/deploy/doppler_program"); 53 | 54 | let (oracle, oracle_account) = keyed_account_for_oracle::( 55 | &mut mollusk, 56 | ADMIN.into(), 57 | "SOL/USDC", 58 | PriceFeed { price: 100_000 }, 59 | ); 60 | 61 | // Accounts 62 | let (system, system_account) = keyed_account_for_system_program(); 63 | let (admin, admin_account) = keyed_account_for_admin(ADMIN.into()); 64 | 65 | // Create oracle account 66 | let create_price_feed_instruction = 67 | solana_system_interface::instruction::create_account_with_seed( 68 | &admin, 69 | &oracle, 70 | &admin, 71 | "SOL/USDC", 72 | oracle_account.lamports, 73 | oracle_account.data.len() as u64, 74 | &doppler_sdk::ID, 75 | ); 76 | 77 | // Update oracle with new values 78 | let oracle_update = Oracle:: { 79 | sequence: 1, // Increment sequence from 0 to 1 80 | payload: PriceFeed { price: 1_100_000 }, 81 | }; 82 | 83 | let price_feed_update_instruction: Instruction = UpdateInstruction { 84 | admin, 85 | oracle_pubkey: oracle, 86 | oracle: oracle_update, 87 | } 88 | .into(); 89 | 90 | MolluskComputeUnitBencher::new(mollusk) 91 | .bench(( 92 | "CreatePriceFeed", 93 | &create_price_feed_instruction, 94 | &[ 95 | (admin, admin_account.clone()), 96 | (oracle, Account::default()), 97 | (system, system_account), 98 | ], 99 | )) 100 | .bench(( 101 | "PriceFeedUpdate", 102 | &price_feed_update_instruction, 103 | &[(admin, admin_account), (oracle, oracle_account)], 104 | )) 105 | .must_pass(true) 106 | .out_dir("benches/") 107 | .execute(); 108 | } 109 | -------------------------------------------------------------------------------- /program/tests/tests.rs: -------------------------------------------------------------------------------- 1 | use doppler::prelude::*; 2 | use doppler_program::PriceFeed; 3 | use doppler_sdk::{Oracle, UpdateInstruction}; 4 | use mollusk_svm::result::Check; 5 | use mollusk_svm::{program::keyed_account_for_system_program, Mollusk}; 6 | use solana_account::{Account, ReadableAccount}; 7 | use solana_clock::Epoch; 8 | use solana_instruction::Instruction; 9 | use solana_pubkey::Pubkey; 10 | 11 | #[must_use] pub fn keyed_account_for_admin(key: Pubkey) -> (Pubkey, Account) { 12 | ( 13 | key, 14 | Account::new(10_000_000_000, 0, &solana_sdk_ids::system_program::ID), 15 | ) 16 | } 17 | 18 | pub fn keyed_account_for_oracle( 19 | mollusk: &mut Mollusk, 20 | admin: Pubkey, 21 | seed: &str, 22 | payload: T, 23 | ) -> (Pubkey, Account) { 24 | let oracle_account = Oracle { 25 | sequence: 0, 26 | payload, 27 | }; 28 | 29 | let key = Pubkey::create_with_seed(&admin, seed, &doppler_sdk::ID).unwrap(); 30 | 31 | let lamports = mollusk 32 | .sysvars 33 | .rent 34 | .minimum_balance(core::mem::size_of::>()); 35 | 36 | let data = oracle_account.to_bytes(); 37 | 38 | let account = Account { 39 | lamports, 40 | data, 41 | owner: doppler_sdk::ID, 42 | executable: false, 43 | rent_epoch: Epoch::default(), 44 | }; 45 | 46 | (key, account) 47 | } 48 | 49 | #[test] 50 | fn test_oracle_update() { 51 | // Create Mollusk instance 52 | let mut mollusk = Mollusk::new(&doppler_sdk::ID, "../target/deploy/doppler_program"); 53 | // Accounts 54 | let (admin, admin_account) = keyed_account_for_admin(ADMIN.into()); 55 | let (oracle, oracle_account) = keyed_account_for_oracle::( 56 | &mut mollusk, 57 | ADMIN.into(), 58 | "SOL/USDC", 59 | PriceFeed { price: 100_000 }, 60 | ); 61 | let (system, system_account) = keyed_account_for_system_program(); 62 | 63 | // Create oracle account 64 | let create_price_feed_instruction = 65 | solana_system_interface::instruction::create_account_with_seed( 66 | &admin, 67 | &oracle, 68 | &admin, 69 | "SOL/USDC", 70 | oracle_account.lamports, 71 | oracle_account.data.len() as u64, 72 | &doppler_sdk::ID, 73 | ); 74 | 75 | // Update oracle with new values 76 | let oracle_update = Oracle:: { 77 | sequence: 1, // Increment sequence from 0 to 1 78 | payload: PriceFeed { price: 1_100_000 }, 79 | }; 80 | 81 | let price_feed_update_instruction: Instruction = UpdateInstruction { 82 | admin, 83 | oracle_pubkey: oracle, 84 | oracle: oracle_update, 85 | } 86 | .into(); 87 | 88 | // Execute instruction 89 | let result = mollusk.process_and_validate_instruction_chain( 90 | &[ 91 | (&create_price_feed_instruction, &[Check::success()]), 92 | (&price_feed_update_instruction, &[Check::success()]), 93 | ], 94 | &vec![ 95 | (admin, admin_account), 96 | (oracle, Account::default()), 97 | (system, system_account), 98 | ], 99 | ); 100 | 101 | // Get updated oracle account 102 | let updated_oracle = result.get_account(&oracle).expect("Missing oracle account"); 103 | 104 | let oracle = Oracle::::from_bytes(updated_oracle.data()); 105 | // Verify the oracle was updated 106 | assert_eq!(&oracle.sequence, &1u64, "Sequence should be updated"); 107 | assert_eq!(&oracle.payload.price, &1_100_000, "Price should be updated"); 108 | } 109 | -------------------------------------------------------------------------------- /examples/src/multiple_price_feed.rs: -------------------------------------------------------------------------------- 1 | use doppler_program::PriceFeed; 2 | use doppler_sdk::{transaction::Builder, Oracle}; 3 | use solana_client::rpc_client::RpcClient; 4 | use solana_keypair::Keypair; 5 | use solana_signer::EncodableKey as _; 6 | use std::path::PathBuf; 7 | 8 | mod constants; 9 | mod fetch; 10 | 11 | fn main() { 12 | // Connect to local Solana cluster 13 | let rpc_url = "http://localhost:8899"; 14 | let client = RpcClient::new(rpc_url.to_string()); 15 | 16 | let keypair_path: PathBuf = [env!("CARGO_MANIFEST_DIR"), "keys", "admin-keypair.json"] 17 | .iter() 18 | .collect(); 19 | 20 | // Load admin keypair (ensure this path is correct) 21 | let admin = Keypair::read_from_file(keypair_path).expect("keypair not found at that path"); 22 | 23 | // Define oracle account public key (replace with actual oracle account) 24 | 25 | let sol_usdc_oracle_data = 26 | fetch::oracle_account::(&client, &constants::SOL_USDC_ORACLE) 27 | .expect("failed to fetch oracle account"); 28 | let sol_usdt_oracle_data = 29 | fetch::oracle_account::(&client, &constants::SOL_USDT_ORACLE) 30 | .expect("failed to fetch oracle account"); 31 | let bonk_sol_oracle_data = 32 | fetch::oracle_account::(&client, &constants::BONK_SOL_ORACLE) 33 | .expect("failed to fetch oracle account"); 34 | 35 | // Create the new price feed data 36 | let new_sol_usdc_price_feed = PriceFeed { 37 | price: sol_usdc_oracle_data.payload.price + 10, 38 | }; 39 | let new_sol_usdt_price_feed = PriceFeed { 40 | price: sol_usdt_oracle_data.payload.price + 10, 41 | }; 42 | let new_bonk_sol_price_feed = PriceFeed { 43 | price: bonk_sol_oracle_data.payload.price + 10, 44 | }; 45 | 46 | // Get a recent blockhash 47 | let recent_blockhash = client 48 | .get_latest_blockhash() 49 | .expect("Failed to get recent blockhash"); 50 | 51 | // Create and sign the transaction 52 | let mut tx_builder = Builder::new(&admin).with_unit_price(1_000); 53 | 54 | // Add multiple oracle updates 55 | for (oracle_pubkey, oracle_data, new_price_feed) in [ 56 | ( 57 | constants::SOL_USDC_ORACLE, 58 | sol_usdc_oracle_data, 59 | new_sol_usdc_price_feed, 60 | ), 61 | ( 62 | constants::SOL_USDT_ORACLE, 63 | sol_usdt_oracle_data, 64 | new_sol_usdt_price_feed, 65 | ), 66 | ( 67 | constants::BONK_SOL_ORACLE, 68 | bonk_sol_oracle_data, 69 | new_bonk_sol_price_feed, 70 | ), 71 | ] { 72 | tx_builder = tx_builder.add_oracle_update( 73 | oracle_pubkey, 74 | Oracle { 75 | sequence: oracle_data.sequence + 1, // New sequence number, must be greater than current 76 | payload: new_price_feed, 77 | }, 78 | ); 79 | } 80 | 81 | let transaction = tx_builder.build(recent_blockhash); 82 | 83 | println!("Sending Tx..."); 84 | 85 | // Send the transaction 86 | let signature = client 87 | .send_and_confirm_transaction(&transaction) 88 | .expect("Failed to send transaction"); 89 | 90 | println!("Transaction successful with signature: {signature:?}"); 91 | 92 | let sol_usdc_oracle_data = 93 | fetch::oracle_account::(&client, &constants::SOL_USDC_ORACLE) 94 | .expect("failed to fetch sol-usdc oracle account"); 95 | let sol_usdt_oracle_data = 96 | fetch::oracle_account::(&client, &constants::SOL_USDT_ORACLE) 97 | .expect("failed to fetch sol-usdt oracle account"); 98 | let bonk_sol_oracle_data = 99 | fetch::oracle_account::(&client, &constants::BONK_SOL_ORACLE) 100 | .expect("failed to fetch bonk-sol oracle account"); 101 | 102 | println!( 103 | "SOL/USDC Price feed : seq : {}, price : {}", 104 | sol_usdc_oracle_data.sequence, sol_usdc_oracle_data.payload.price 105 | ); 106 | println!( 107 | "SOL/USDT Price feed : seq : {}, price : {}", 108 | sol_usdt_oracle_data.sequence, sol_usdt_oracle_data.payload.price 109 | ); 110 | println!( 111 | "Bonk/SOL Price feed : seq : {}, price : {}", 112 | bonk_sol_oracle_data.sequence, bonk_sol_oracle_data.payload.price 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /sdk/src/accounts.rs: -------------------------------------------------------------------------------- 1 | use solana_instruction::{AccountMeta, Instruction}; 2 | use solana_pubkey::Pubkey; 3 | 4 | use crate::constants::{ADMIN_VERIFICATION_CU, ID, PAYLOAD_WRITE_CU, SEQUENCE_CHECK_CU}; 5 | 6 | #[repr(C)] 7 | #[derive(Clone, Copy, Debug)] 8 | pub struct Oracle { 9 | pub sequence: u64, 10 | pub payload: T, 11 | } 12 | 13 | impl Oracle { 14 | pub fn to_bytes(&self) -> Vec { 15 | let mut data = Vec::with_capacity(core::mem::size_of::()); 16 | // write sequence bytes 17 | data.extend_from_slice(&self.sequence.to_le_bytes()); 18 | // write payload bytes 19 | data.extend_from_slice(unsafe { 20 | core::slice::from_raw_parts( 21 | core::ptr::from_ref(&self.payload).cast::(), 22 | core::mem::size_of::(), 23 | ) 24 | }); 25 | data 26 | } 27 | 28 | #[must_use] 29 | pub fn from_bytes(data: &[u8]) -> Self { 30 | assert!(data.len() == core::mem::size_of::()); 31 | 32 | // read u64 sequence from first 8 bytes 33 | let mut seq_bytes = [0u8; 8]; 34 | seq_bytes.copy_from_slice(&data[..8]); 35 | let sequence = u64::from_le_bytes(seq_bytes); 36 | 37 | // read payload from remaining bytes 38 | let payload = unsafe { *data[8..].as_ptr().cast::() }; 39 | 40 | Self { sequence, payload } 41 | } 42 | } 43 | 44 | pub struct UpdateInstruction { 45 | pub admin: Pubkey, 46 | pub oracle_pubkey: Pubkey, 47 | pub oracle: Oracle, 48 | } 49 | 50 | impl UpdateInstruction { 51 | pub const fn compute_units(&self) -> u32 { 52 | SEQUENCE_CHECK_CU 53 | + ADMIN_VERIFICATION_CU 54 | + PAYLOAD_WRITE_CU 55 | + (core::mem::size_of::>() / 4) as u32 56 | } 57 | 58 | pub const fn loaded_accounts_data_size_limit(&self) -> u32 { 59 | core::mem::size_of::>() as u32 60 | } 61 | } 62 | 63 | impl From> for Instruction { 64 | fn from(update: UpdateInstruction) -> Self { 65 | let data = update.oracle.to_bytes(); 66 | 67 | Self { 68 | program_id: ID, 69 | accounts: vec![ 70 | AccountMeta::new_readonly(update.admin, true), 71 | AccountMeta::new(update.oracle_pubkey, false), 72 | ], 73 | data, 74 | } 75 | } 76 | } 77 | 78 | #[cfg(test)] 79 | mod tests { 80 | use doppler_program::PriceFeed; 81 | use solana_pubkey::Pubkey; 82 | 83 | use super::*; 84 | 85 | #[repr(C)] 86 | #[derive(Clone, Copy)] 87 | struct PropAMM { 88 | pub bid: u64, 89 | pub ask: u64, 90 | } 91 | 92 | #[repr(C)] 93 | #[derive(Clone, Copy)] 94 | struct MarketData { 95 | pub price: u64, 96 | pub volume: u64, 97 | pub confidence: u32, 98 | } 99 | 100 | #[test] 101 | fn test_oracle_to_bytes() { 102 | let oracle = Oracle { 103 | sequence: 42, 104 | payload: 123u32, 105 | }; 106 | 107 | let bytes = oracle.to_bytes(); 108 | assert_eq!(bytes.len(), 12); 109 | assert_eq!(&bytes[0..8], &42u64.to_le_bytes()); 110 | assert_eq!(&bytes[8..12], &123u32.to_le_bytes()); 111 | } 112 | 113 | #[test] 114 | fn test_cu_limit_num_payload() { 115 | let admin = Pubkey::new_unique(); 116 | let oracle_pubkey = Pubkey::new_unique(); 117 | 118 | let oracle = Oracle { 119 | sequence: 1, 120 | payload: 789u64, 121 | }; 122 | 123 | let update_instruction = UpdateInstruction { 124 | admin, 125 | oracle_pubkey, 126 | oracle, 127 | }; 128 | 129 | let compute_instruction = update_instruction.compute_units(); 130 | 131 | assert_eq!(compute_instruction, 21); 132 | } 133 | 134 | #[test] 135 | fn test_cu_limit_price_feed_payload() { 136 | let admin = Pubkey::new_unique(); 137 | let oracle_pubkey = Pubkey::new_unique(); 138 | 139 | let oracle = Oracle { 140 | sequence: 1, 141 | payload: PriceFeed { price: 1_100_000 }, 142 | }; 143 | 144 | let update_instruction = UpdateInstruction { 145 | admin, 146 | oracle_pubkey, 147 | oracle, 148 | }; 149 | 150 | let compute_instruction = update_instruction.compute_units(); 151 | 152 | assert_eq!(compute_instruction, 21); 153 | } 154 | 155 | #[test] 156 | fn test_cu_limit_prop_amm_payload() { 157 | let admin = Pubkey::new_unique(); 158 | let oracle_pubkey = Pubkey::new_unique(); 159 | 160 | let oracle = Oracle { 161 | sequence: 1, 162 | payload: PropAMM { 163 | bid: 10_500_000, 164 | ask: 10_550_000, 165 | }, 166 | }; 167 | 168 | let update_instruction = UpdateInstruction { 169 | admin, 170 | oracle_pubkey, 171 | oracle, 172 | }; 173 | 174 | let compute_instruction = update_instruction.compute_units(); 175 | 176 | assert_eq!(compute_instruction, 23); 177 | } 178 | 179 | #[test] 180 | fn test_cu_limit_market_data_payload() { 181 | let admin = Pubkey::new_unique(); 182 | let oracle_pubkey = Pubkey::new_unique(); 183 | 184 | let oracle = Oracle { 185 | sequence: 1, 186 | payload: MarketData { 187 | price: 45_000_000, 188 | volume: 150_000_000, 189 | confidence: 300, 190 | }, 191 | }; 192 | 193 | let update_instruction = UpdateInstruction { 194 | admin, 195 | oracle_pubkey, 196 | oracle, 197 | }; 198 | 199 | let compute_instruction = update_instruction.compute_units(); 200 | 201 | assert_eq!(compute_instruction, 25); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./assets/logo.svg) 2 | 3 |

4 | A 21 CU Solana Oracle Program 5 |

6 | 7 | ## Overview 8 | 9 | Doppler is an ultra-optimized oracle program for Solana, achieving unparalleled performance at just **21 Compute Units (CUs)** per update. Built with low-level optimizations and minimal overhead, Doppler sets the standard for high-frequency, low-latency price feeds on Solana. 10 | 11 | ## Features 12 | 13 | - **21 CU Oracle Updates**: The most efficient oracle implementation on Solana 14 | - **Generic Payload Support**: Flexible data structure supporting any payload type 15 | - **Sequence-Based Updates**: Built-in replay protection and ordering guarantees 16 | - **Zero Dependencies**: Pure no_std Rust implementation for minimal overhead 17 | - **Direct Memory Operations**: Optimized assembly-level exits for maximum efficiency 18 | 19 | ## Installation 20 | 21 | Add Doppler SDK and required Solana crates to your `Cargo.toml`: 22 | 23 | ```toml 24 | [dependencies] 25 | doppler-sdk = "0.1.0" 26 | solana-instruction = "2.3.0" 27 | solana-pubkey = "2.3.0" 28 | solana-compute-budget-interface = "2.2.2" 29 | solana-transaction = "2.3.0" 30 | solana-keypair = "2.3.0" 31 | solana-signer = "2.2.1" 32 | # Add other Solana crates as needed 33 | ``` 34 | 35 | ## Program ID 36 | 37 | ``` 38 | fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm 39 | ``` 40 | 41 | ## Architecture 42 | 43 | Doppler uses a simple yet powerful architecture: 44 | 45 | 1. **Admin Account**: Controls oracle updates (hardcoded for security) 46 | 2. **Oracle Account**: Stores the sequence number and payload data 47 | 3. **Sequence Validation**: Ensures updates are monotonically increasing 48 | 49 | ### Data Structure 50 | 51 | ```rust 52 | pub struct Oracle { 53 | pub sequence: u64, // Timestamp, slot height, or auto-increment 54 | pub payload: T, // Your custom data structure 55 | } 56 | ``` 57 | 58 | ## Usage Guide 59 | 60 | ### 1. Setting Up Compute Budget 61 | 62 | To achieve the 21 CU performance, configure your transaction with appropriate compute budget: 63 | 64 | ```rust 65 | use solana_compute_budget_interface::ComputeBudgetInstruction; 66 | use solana_instruction::Instruction; 67 | use solana_transaction::Transaction; 68 | 69 | // Request exactly the CUs needed (21 + overhead for other instructions) 70 | let compute_budget_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); 71 | 72 | // Add to your transaction 73 | let mut instructions = vec![compute_budget_ix]; 74 | ``` 75 | 76 | ### 2. Setting Priority Fees 77 | 78 | For high-frequency oracle updates, use priority fees to ensure timely inclusion: 79 | 80 | ```rust 81 | // Set priority fee (price per compute unit in micro-lamports) 82 | let priority_fee_ix = ComputeBudgetInstruction::set_compute_unit_price(1000); 83 | 84 | instructions.push(priority_fee_ix); 85 | ``` 86 | 87 | ### 3. Optimizing Account Data Size 88 | 89 | Use `setLoadedAccountsDataSizeLimit` to optimize memory allocation: 90 | 91 | ```rust 92 | // Set the maximum loaded account data size 93 | // Calculate based on your oracle data structure size 94 | let data_size_limit_ix = ComputeBudgetInstruction::set_loaded_accounts_data_size_limit( 95 | 32_768 // 32KB is usually sufficient for oracle operations 96 | ); 97 | 98 | instructions.push(data_size_limit_ix); 99 | ``` 100 | 101 | ### 4. Creating an Oracle Update 102 | 103 | ```rust 104 | use doppler_sdk::{Oracle, UpdateInstruction, ID as DOPPLER_ID}; 105 | use solana_instruction::Instruction; 106 | use solana_pubkey::Pubkey; 107 | 108 | // Define your payload structure 109 | #[derive(Clone, Copy)] 110 | pub struct PriceFeed { 111 | pub price: u64, 112 | } 113 | 114 | // Create oracle update 115 | let oracle_update = Oracle { 116 | sequence: 1234567890, // Must be > current sequence 117 | payload: PriceFeed { 118 | price: 42_000_000, // $42.00 with 6 decimals 119 | }, 120 | }; 121 | 122 | // Create update instruction 123 | let update_ix: Instruction = UpdateInstruction { 124 | admin: admin_pubkey, 125 | oracle_pubkey: oracle_pubkey, 126 | oracle: oracle_update, 127 | }.into(); 128 | 129 | // Add to instructions 130 | instructions.push(update_ix); 131 | ``` 132 | 133 | ### 5. Complete Transaction Example 134 | 135 | ```rust 136 | use doppler_sdk::{Oracle, UpdateInstruction}; 137 | use solana_client::rpc_client::RpcClient; 138 | use solana_compute_budget_interface::ComputeBudgetInstruction; 139 | use solana_instruction::Instruction; 140 | use solana_keypair::Keypair; 141 | use solana_signer::Signer; 142 | use solana_transaction::Transaction; 143 | 144 | async fn update_oracle( 145 | client: &RpcClient, 146 | admin: &Keypair, 147 | oracle_pubkey: Pubkey, 148 | new_price: u64, 149 | sequence: u64, 150 | ) -> Result<(), Box> { 151 | // Build all instructions 152 | let mut instructions = vec![ 153 | // 1. Set compute budget 154 | ComputeBudgetInstruction::set_compute_unit_limit(200_000), 155 | 156 | // 2. Set priority fee (1000 micro-lamports per CU) 157 | ComputeBudgetInstruction::set_compute_unit_price(1_000), 158 | 159 | // 3. Set loaded accounts data size limit 160 | ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(32_768), 161 | ]; 162 | 163 | // 4. Add oracle update 164 | let oracle_update = Oracle { 165 | sequence, 166 | payload: PriceFeed { price: new_price }, 167 | }; 168 | 169 | let update_ix: Instruction = UpdateInstruction { 170 | admin: admin.pubkey(), 171 | oracle_pubkey, 172 | oracle: oracle_update, 173 | }.into(); 174 | 175 | instructions.push(update_ix); 176 | 177 | // Create and send transaction 178 | let recent_blockhash = client.get_latest_blockhash()?; 179 | let tx = Transaction::new_signed_with_payer( 180 | &instructions, 181 | Some(&admin.pubkey()), 182 | &[admin], 183 | recent_blockhash, 184 | ); 185 | 186 | let signature = client.send_and_confirm_transaction(&tx)?; 187 | println!("Oracle updated: {}", signature); 188 | 189 | Ok(()) 190 | } 191 | ``` 192 | 193 | ## Performance Optimization Tips 194 | 195 | ### 1. Compute Budget Configuration 196 | 197 | - **Exact CU Request**: Request only what you need (21 CUs + overhead) 198 | - **Priority Fees**: Use dynamic priority fees based on network congestion 199 | - **Account Data Size**: Minimize loaded data to reduce memory overhead 200 | 201 | ### 2. Batching Updates 202 | 203 | For multiple oracle updates, batch them efficiently: 204 | 205 | ```rust 206 | // DON'T: Multiple transactions 207 | for oracle in oracles { 208 | send_update(oracle)?; // 21 CU each, but multiple transactions 209 | } 210 | 211 | // DO: Single transaction with multiple updates 212 | let mut instructions = vec![ 213 | ComputeBudgetInstruction::set_compute_unit_limit(200_000), 214 | ComputeBudgetInstruction::set_compute_unit_price(1_000), 215 | ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(65_536), 216 | ]; 217 | 218 | for oracle in oracles { 219 | instructions.push(create_update_instruction(oracle)); 220 | } 221 | // Single transaction with all updates 222 | ``` 223 | 224 | ### 3. Network Optimization 225 | 226 | ```rust 227 | // Use getRecentPrioritizationFees to determine optimal fee 228 | let recent_fees = client.get_recent_prioritization_fees(&[oracle_pubkey])?; 229 | let optimal_fee = calculate_optimal_fee(recent_fees); 230 | 231 | let priority_ix = ComputeBudgetInstruction::set_compute_unit_price(optimal_fee); 232 | ``` 233 | 234 | ## Testing 235 | 236 | ### Unit 237 | 238 | Run the test suite: 239 | 240 | ```bash 241 | # Run all tests 242 | cargo test 243 | ``` 244 | 245 | ### E2E 246 | 247 | ```bash 248 | ./test-validator.sh 249 | 250 | cargo run --bin single-price-feed 251 | cargo run --bin multiple-price-feed 252 | ``` 253 | 254 | example of single price feed update response 255 | 256 | ``` 257 | Transaction executed in slot 131: 258 | Block Time: 2025-09-03T04:23:08+03:00 259 | Version: legacy 260 | Recent Blockhash: 89ZvpNezGugkfm9LnN99rhb6aTNaW1cLKkS2DDbr7NPA 261 | Signature 0: m14zQFvt1jU9YYM2QAmVSnMZUa5P2eKdtP21Shu9w9kEhxKLAfJoUyqZwiTt43hGwewhsahQJi5eLJ71NptUWDu 262 | Account 0: srw- admnz5UvRa93HM5nTrxXmsJ1rw2tvXMBFGauvCgzQhE (fee payer) 263 | Account 1: -rw- QUVF91dzXWYvE5FmFEc41JZxRDmNgx8S8P6sNDWYZiW 264 | Account 2: -r-x ComputeBudget111111111111111111111111111111 265 | Account 3: -r-x fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm 266 | Instruction 0 267 | Program: ComputeBudget111111111111111111111111111111 (2) 268 | Data: [3, 232, 3, 0, 0, 0, 0, 0, 0] 269 | Instruction 1 270 | Program: ComputeBudget111111111111111111111111111111 (2) 271 | Data: [2, 215, 1, 0, 0] 272 | Instruction 2 273 | Program: ComputeBudget111111111111111111111111111111 (2) 274 | Data: [4, 127, 0, 0, 0] 275 | Instruction 3 276 | Program: fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm (3) 277 | Account 0: admnz5UvRa93HM5nTrxXmsJ1rw2tvXMBFGauvCgzQhE (0) 278 | Account 1: QUVF91dzXWYvE5FmFEc41JZxRDmNgx8S8P6sNDWYZiW (1) 279 | Data: [159, 136, 1, 0, 0, 0, 0, 0, 64, 226, 1, 0, 0, 0, 0, 0, 160, 213, 119, 107, 1, 0, 0, 0] 280 | Status: Ok 281 | Fee: ◎0.000005001 282 | Account 0 balance: ◎9.999969996 -> ◎9.999964995 283 | Account 1 balance: ◎0.00100224 284 | Account 2 balance: ◎0.000000001 285 | Account 3 balance: ◎0.00114144 286 | Compute Units Consumed: 471 287 | Log Messages: 288 | Program ComputeBudget111111111111111111111111111111 invoke [1] 289 | Program ComputeBudget111111111111111111111111111111 success 290 | Program ComputeBudget111111111111111111111111111111 invoke [1] 291 | Program ComputeBudget111111111111111111111111111111 success 292 | Program ComputeBudget111111111111111111111111111111 invoke [1] 293 | Program ComputeBudget111111111111111111111111111111 success 294 | Program fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm invoke [1] 295 | Program fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm consumed 21 of 21 compute units 296 | Program fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm success 297 | 298 | Finalized 299 | ``` 300 | 301 | > Fully fledged tx requires: `471 CU` + `111 bytes` 302 | 303 | example of multiple price feed update response 304 | 305 | ``` 306 | Transaction executed in slot 218: 307 | Block Time: 2025-09-06T13:06:05+03:00 308 | Version: legacy 309 | Recent Blockhash: AeCvWYJjrx6Yxjknh6ndTTaTYsHkPQgr9iMURRN8Ah4S 310 | Signature 0: 3MLXk7YCsqEoMiYiGT4RYKa3Js2QJ6acM1BQstKGNbXsUJ6rNaySmUzzqNRDnFd7St1XTpPngAbcnf3ZxD2Lj9Jr 311 | Account 0: srw- admnz5UvRa93HM5nTrxXmsJ1rw2tvXMBFGauvCgzQhE (fee payer) 312 | Account 1: -rw- QUVF91dzXWYvE5FmFEc41JZxRDmNgx8S8P6sNDWYZiW 313 | Account 2: -rw- 6uQ848roY5vumz43QeQguE7xCyBSmgZbwNdJMTrs2Xhy 314 | Account 3: -rw- 9bA7GPqPpZ5aLbwb8E6cKvUPM8pcHXXTqLpf5zLAqHP5 315 | Account 4: -r-x ComputeBudget111111111111111111111111111111 316 | Account 5: -r-x fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm 317 | Instruction 0 318 | Program: ComputeBudget111111111111111111111111111111 (4) 319 | Data: [3, 232, 3, 0, 0, 0, 0, 0, 0] 320 | Instruction 1 321 | Program: ComputeBudget111111111111111111111111111111 (4) 322 | Data: [4, 175, 0, 0, 0] 323 | Instruction 2 324 | Program: ComputeBudget111111111111111111111111111111 (4) 325 | Data: [2, 1, 2, 0, 0] 326 | Instruction 3 327 | Program: fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm (5) 328 | Account 0: admnz5UvRa93HM5nTrxXmsJ1rw2tvXMBFGauvCgzQhE (0) 329 | Account 1: QUVF91dzXWYvE5FmFEc41JZxRDmNgx8S8P6sNDWYZiW (1) 330 | Data: [2, 0, 0, 0, 0, 0, 0, 0, 180, 134, 1, 0, 0, 0, 0, 0] 331 | Instruction 4 332 | Program: fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm (5) 333 | Account 0: admnz5UvRa93HM5nTrxXmsJ1rw2tvXMBFGauvCgzQhE (0) 334 | Account 1: 9bA7GPqPpZ5aLbwb8E6cKvUPM8pcHXXTqLpf5zLAqHP5 (3) 335 | Data: [1, 0, 0, 0, 0, 0, 0, 0, 170, 134, 1, 0, 0, 0, 0, 0] 336 | Instruction 5 337 | Program: fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm (5) 338 | Account 0: admnz5UvRa93HM5nTrxXmsJ1rw2tvXMBFGauvCgzQhE (0) 339 | Account 1: 6uQ848roY5vumz43QeQguE7xCyBSmgZbwNdJMTrs2Xhy (2) 340 | Data: [1, 0, 0, 0, 0, 0, 0, 0, 170, 134, 1, 0, 0, 0, 0, 0] 341 | Status: Ok 342 | Fee: ◎0.000005001 343 | Account 0 balance: ◎0.999994999 -> ◎0.999989998 344 | Account 1 balance: ◎0.00100224 345 | Account 2 balance: ◎0.00100224 346 | Account 3 balance: ◎0.00100224 347 | Account 4 balance: ◎0.000000001 348 | Account 5 balance: ◎0.00114144 349 | Compute Units Consumed: 513 350 | Log Messages: 351 | Program ComputeBudget111111111111111111111111111111 invoke [1] 352 | Program ComputeBudget111111111111111111111111111111 success 353 | Program ComputeBudget111111111111111111111111111111 invoke [1] 354 | Program ComputeBudget111111111111111111111111111111 success 355 | Program ComputeBudget111111111111111111111111111111 invoke [1] 356 | Program ComputeBudget111111111111111111111111111111 success 357 | Program fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm invoke [1] 358 | Program fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm consumed 21 of 63 compute units 359 | Program fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm success 360 | Program fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm invoke [1] 361 | Program fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm consumed 21 of 42 compute units 362 | Program fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm success 363 | Program fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm invoke [1] 364 | Program fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm consumed 21 of 21 compute units 365 | Program fastRQJt3nLdY3QA7n8eZ8ETEVefy56ryfUGVkfZokm success 366 | 367 | Finalized 368 | ``` 369 | 370 | ### Expected Priority Score 371 | 372 | based on the [Anza's blog post](https://www.anza.xyz/blog/cu-optimization-with-setloadedaccountsdatasizelimit) and the code from [single price feed update example](https://github.com/blueshift-gg/doppler/blob/master/examples/src/single_price_feed.rs) 373 | 374 | let's assume we are going to update a single oracle: 375 | 376 | - 1 signature 377 | - 0 write locks 378 | - Requested compute-budget-limit to 21 (with compute-budget instructions 321 and 471 respectively) CUs 379 | - Paying priority fee: 1.00 lamports per CU 380 | 381 | | Metric | Without Instruction | With 111 byte Limit | 382 | | ------------------------------ | -------------------------------- | --------------------------------- | 383 | | Loaded Account Data Size Limit | 64M | 111 bytes | 384 | | Data Size Cost Calculation | 64M x (8/32K) | 111 bytes x (8/32K) | 385 | | Data Size Cost (CUs) | 16,000 | 0.02775 | 386 | | Reward to Leader Calculation | (1 x 5000 + 1 x 321)/2 | (1 x 5000 + 1 x 471)/2 | 387 | | Reward to Leader (lamports) | 2,660.5 | 2,735.5 | 388 | | Transaction Cost Formula | 1 x 720 + 0 x 300 + 321 + 16,000 | 1 x 720 + 0 x 300 + 471 + 0.02775 | 389 | | Transaction Cost (CUs) | 17,041 | 1,141.02775 | 390 | | Priority Score | 0.156 | 2.397 | 391 | 392 | ## Building 393 | 394 | Build the on-chain program: 395 | 396 | ```bash 397 | # Build for Solana BPF 398 | cargo build-sbf 399 | 400 | # Deploy 401 | solana program deploy target/deploy/doppler.so 402 | ``` 403 | 404 | ## Security Considerations 405 | 406 | 1. **Admin Key**: The admin key is hardcoded in the program for security 407 | 2. **Sequence Validation**: Prevents replay attacks and ensures ordering 408 | 3. **No External Dependencies**: Reduces attack surface 409 | 4. **Direct Memory Operations**: Eliminates unnecessary abstraction layers 410 | 411 | ## Benchmarks 412 | 413 | | Operation | Compute Units | 414 | | ------------------ | ------------- | 415 | | Oracle Update | 21 | 416 | | Sequence Check | 5 | 417 | | Payload Write | 10 | 418 | | Admin Verification | 6 | 419 | 420 | ## Example Payloads 421 | 422 | ### Simple Price Feed 423 | 424 | ```rust 425 | #[derive(Clone, Copy)] 426 | pub struct PriceFeed { 427 | pub price: u64, 428 | } 429 | ``` 430 | 431 | ### AMM Oracle 432 | 433 | ```rust 434 | #[derive(Clone, Copy)] 435 | pub struct PropAMM { 436 | pub bid: u64, 437 | pub ask: u64, 438 | } 439 | ``` 440 | 441 | ### Complex Market Data 442 | 443 | ```rust 444 | #[derive(Clone, Copy)] 445 | pub struct MarketData { 446 | pub price: u64, 447 | pub volume: u64, 448 | pub confidence: u32, 449 | } 450 | ``` 451 | 452 | ## FAQ 453 | 454 | **Q: Why only 21 CUs?** 455 | A: Doppler uses direct memory operations, inline assembly optimizations, and zero-overhead abstractions to achieve minimal compute usage. 456 | 457 | **Q: Can I use custom payload types?** 458 | A: Yes! Doppler is generic over any `Copy` type. Define your structure and use it with the SDK. 459 | 460 | **Q: How do I handle oracle account creation?** 461 | A: However you like, but if you use Solana's `create_account_with_seed` instruction with the admin as the base key it's cheaper! 462 | 463 | **Q: What's the maximum update frequency?** 464 | A: Limited only by Solana's throughput. With 21 CUs, you can update as fast as you land. 465 | 466 | ## Support 467 | 468 | For issues, questions, or contributions: 469 | 470 | - GitHub: [@blueshift-gg](https://github.com/blueshift-gg) 471 | - X: [@blueshift](https://x.com/blueshift) 472 | - Discord: [discord.gg/blueshift](https://discord.gg/blueshift) 473 | 474 | ## License 475 | 476 | Licensed under [MIT](./LICENSE). 477 | --------------------------------------------------------------------------------