From c80a9c0b2b815ee5331be33a09613d0f95871330 Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Thu, 12 Dec 2024 11:49:53 -0800 Subject: [PATCH 1/3] refactor to multi update --- programs/drift/src/error.rs | 4 +- .../src/instructions/pyth_lazer_oracle.rs | 125 ++++++++++++------ programs/drift/src/lib.rs | 7 +- sdk/src/driftClient.ts | 28 ++-- sdk/src/idl/drift.json | 15 +-- tests/placeAndMakeSwiftPerpBankrun.ts | 6 +- tests/pythLazer.ts | 44 +++++- tests/pythLazerBankrun.ts | 55 +++++++- tests/pythLazerData.ts | 3 + 9 files changed, 202 insertions(+), 85 deletions(-) diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index 5ed323e20..7a0705188 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -559,9 +559,9 @@ pub enum ErrorCode { OracleWrongVaaOwner, #[msg("Multi updates must have 2 or fewer accounts passed in remaining accounts")] OracleTooManyPriceAccountUpdates, - #[msg("Don't have the same remaining accounts number and merkle price updates left")] + #[msg("Don't have the same remaining accounts number and pyth updates left")] OracleMismatchedVaaAndPriceUpdates, - #[msg("Remaining account passed is not a valid pda")] + #[msg("Remaining account passed does not match oracle update derived pda")] OracleBadRemainingAccountPublicKey, #[msg("FailedOpenbookV2CPI")] FailedOpenbookV2CPI, diff --git a/programs/drift/src/instructions/pyth_lazer_oracle.rs b/programs/drift/src/instructions/pyth_lazer_oracle.rs index 432f6dc6e..bff7e2363 100644 --- a/programs/drift/src/instructions/pyth_lazer_oracle.rs +++ b/programs/drift/src/instructions/pyth_lazer_oracle.rs @@ -1,17 +1,20 @@ use crate::error::ErrorCode; +use crate::math::casting::Cast; +use crate::math::safe_math::SafeMath; use crate::state::pyth_lazer_oracle::{ PythLazerOracle, PYTH_LAZER_ORACLE_SEED, PYTH_LAZER_STORAGE_ID, }; use crate::validate; use anchor_lang::prelude::*; use pyth_lazer_sdk::protocol::payload::{PayloadData, PayloadPropertyValue}; +use pyth_lazer_sdk::protocol::router::Price; use solana_program::sysvar::instructions::load_current_index_checked; -pub fn handle_update_pyth_lazer_oracle( - ctx: Context, - feed_id: u32, +pub fn handle_update_pyth_lazer_oracle<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdatePythLazerOracle>, pyth_message: Vec, ) -> Result<()> { + // Verify the Pyth lazer message let ix_idx = load_current_index_checked(&ctx.accounts.ix_sysvar.to_account_info())?; validate!( ix_idx > 0, @@ -25,63 +28,101 @@ pub fn handle_update_pyth_lazer_oracle( &pyth_message, ix_idx - 1, 0, - 1, + 0, ); + if verified.is_err() { msg!("{:?}", verified); return Err(ErrorCode::UnverifiedPythLazerMessage.into()); } + // Load oracle accounts from remaining accounts + let remaining_accounts = ctx.remaining_accounts; + validate!( + remaining_accounts.len() <= 3, + ErrorCode::OracleTooManyPriceAccountUpdates + )?; + let data = PayloadData::deserialize_slice_le(verified.unwrap().payload) .map_err(|_| ProgramError::InvalidInstructionData)?; + let next_timestamp = data.timestamp_us.0; - if data.feeds.is_empty() || data.feeds[0].properties.is_empty() { - msg!("Invalid Pyth lazer message. No feeds or properties found"); - return Err(ErrorCode::InvalidPythLazerMessage.into()); - } + validate!( + remaining_accounts.len() == data.feeds.len(), + ErrorCode::OracleMismatchedVaaAndPriceUpdates + )?; - if data.feeds[0].feed_id.0 != feed_id { - msg!( - "Feed ID mismatch. Expected {} but got {}", - feed_id, - data.feeds[0].feed_id.0 - ); - return Err(ErrorCode::InvalidPythLazerMessage.into()); - } + for (account, payload_data) in remaining_accounts.iter().zip(data.feeds.iter()) { + let pyth_lazer_oracle_loader: AccountLoader = + AccountLoader::try_from(account)?; + let mut pyth_lazer_oracle = pyth_lazer_oracle_loader.load_mut()?; - let mut pyth_lazer_oracle = ctx.accounts.pyth_lazer_oracle.load_mut()?; - let current_timestamp = pyth_lazer_oracle.publish_time; - let next_timestamp = data.timestamp_us.0; + let feed_id = payload_data.feed_id.0; - if next_timestamp > current_timestamp { - let PayloadPropertyValue::Price(Some(price)) = data.feeds[0].properties[0] else { - return Err(ErrorCode::InvalidPythLazerMessage.into()); - }; - pyth_lazer_oracle.price = price.0.get(); - pyth_lazer_oracle.posted_slot = Clock::get()?.slot; - pyth_lazer_oracle.publish_time = next_timestamp; - pyth_lazer_oracle.conf = 0; - pyth_lazer_oracle.exponent = -8; - msg!("Price updated to {}", price.0.get()); - - msg!( - "Posting new lazer update. current ts {} < next ts {}", - current_timestamp, - next_timestamp - ); - } else { - msg!( - "Skipping new lazer update. current ts {} >= next ts {}", - current_timestamp, - next_timestamp + // Verify the pda + let pda = Pubkey::find_program_address( + &[PYTH_LAZER_ORACLE_SEED, &feed_id.to_le_bytes()], + &crate::ID, + ) + .0; + require_keys_eq!( + *account.key, + pda, + ErrorCode::OracleBadRemainingAccountPublicKey ); + + let current_timestamp = pyth_lazer_oracle.publish_time; + + if next_timestamp > current_timestamp { + let PayloadPropertyValue::Price(Some(price)) = payload_data.properties[0] else { + return Err(ErrorCode::InvalidPythLazerMessage.into()); + }; + + let mut best_bid_price: Option = None; + let mut best_ask_price: Option = None; + + for property in &payload_data.properties { + match property { + PayloadPropertyValue::BestBidPrice(price) => best_bid_price = *price, + PayloadPropertyValue::BestAskPrice(price) => best_ask_price = *price, + _ => {} + } + } + + // Default to 2% of the price for conf if bid > ask or one-sided market + let mut conf: i64 = price.0.get().safe_div(50)?; + if let (Some(bid), Some(ask)) = (best_bid_price, best_ask_price) { + if bid.0.get() < ask.0.get() { + conf = ask.0.get() - bid.0.get(); + } + } + + pyth_lazer_oracle.price = price.0.get(); + pyth_lazer_oracle.posted_slot = Clock::get()?.slot; + pyth_lazer_oracle.publish_time = next_timestamp; + pyth_lazer_oracle.conf = conf.cast::()?; + pyth_lazer_oracle.exponent = -8; + msg!("Price updated to {}", price.0.get()); + + msg!( + "Posting new lazer update. current ts {} < next ts {}", + current_timestamp, + next_timestamp + ); + } else { + msg!( + "Skipping new lazer update. current ts {} >= next ts {}", + current_timestamp, + next_timestamp + ); + } } Ok(()) } #[derive(Accounts)] -#[instruction(feed_id: u32, pyth_message: Vec)] +#[instruction(pyth_message: Vec)] pub struct UpdatePythLazerOracle<'info> { #[account(mut)] pub keeper: Signer<'info>, @@ -93,6 +134,4 @@ pub struct UpdatePythLazerOracle<'info> { /// CHECK: checked by ed25519 verify #[account(address = solana_program::sysvar::instructions::ID)] pub ix_sysvar: AccountInfo<'info>, - #[account(mut, seeds = [PYTH_LAZER_ORACLE_SEED, &feed_id.to_le_bytes()], bump)] - pub pyth_lazer_oracle: AccountLoader<'info, PythLazerOracle>, } diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 756855778..444b11117 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1567,12 +1567,11 @@ pub mod drift { handle_initialize_pyth_lazer_oracle(ctx, feed_id) } - pub fn post_pyth_lazer_oracle_update( - ctx: Context, - feed_id: u32, + pub fn post_pyth_lazer_oracle_update<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdatePythLazerOracle>, pyth_message: Vec, ) -> Result<()> { - handle_update_pyth_lazer_oracle(ctx, feed_id, pyth_message) + handle_update_pyth_lazer_oracle(ctx, pyth_message) } pub fn initialize_high_leverage_mode_config( diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 400a9f8c2..29abf5d2f 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -8606,11 +8606,11 @@ export class DriftClient { } public async postPythLazerOracleUpdate( - feedId: number, + feedIds: number[], pythMessageHex: string ): Promise { const postIxs = this.getPostPythLazerOracleUpdateIxs( - feedId, + feedIds, pythMessageHex, undefined, 2 @@ -8621,37 +8621,37 @@ export class DriftClient { } public getPostPythLazerOracleUpdateIxs( - feedId: number, + feedIds: number[], pythMessageHex: string, precedingIxs: TransactionInstruction[] = [], overrideIxCount?: number ): TransactionInstruction[] { const pythMessageBytes = Buffer.from(pythMessageHex, 'hex'); - const messageOffset = 1; - - const updateData = new Uint8Array(1 + pythMessageBytes.length); - updateData[0] = feedId; - updateData.set(pythMessageBytes, 1); + const updateData = new Uint8Array(pythMessageBytes); const verifyIx = createMinimalEd25519VerifyIx( overrideIxCount || precedingIxs.length + 1, - messageOffset, + 0, updateData ); + const remainingAccountsMeta = feedIds.map((feedId) => { + return { + pubkey: getPythLazerOraclePublicKey(this.program.programId, feedId), + isSigner: false, + isWritable: true, + }; + }); + const ix = this.program.instruction.postPythLazerOracleUpdate( - feedId, pythMessageBytes, { accounts: { keeper: this.wallet.publicKey, pythLazerStorage: PYTH_LAZER_STORAGE_ACCOUNT_KEY, - pythLazerOracle: getPythLazerOraclePublicKey( - this.program.programId, - feedId - ), ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, }, + remainingAccounts: remainingAccountsMeta, } ); return [verifyIx, ix]; diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 20bdb94f3..62b9efe71 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -6503,25 +6503,16 @@ }, { "name": "pythLazerStorage", - "isMut": true, + "isMut": false, "isSigner": false }, { "name": "ixSysvar", "isMut": false, "isSigner": false - }, - { - "name": "pythLazerOracle", - "isMut": true, - "isSigner": false } ], "args": [ - { - "name": "feedId", - "type": "u32" - }, { "name": "pythMessage", "type": "bytes" @@ -14290,12 +14281,12 @@ { "code": 6277, "name": "OracleMismatchedVaaAndPriceUpdates", - "msg": "Don't have the same remaining accounts number and merkle price updates left" + "msg": "Don't have the same remaining accounts number and pyth updates left" }, { "code": 6278, "name": "OracleBadRemainingAccountPublicKey", - "msg": "Remaining account passed is not a valid pda" + "msg": "Remaining account passed does not match oracle update derived pda" }, { "code": 6279, diff --git a/tests/placeAndMakeSwiftPerpBankrun.ts b/tests/placeAndMakeSwiftPerpBankrun.ts index fea69af07..88797108a 100644 --- a/tests/placeAndMakeSwiftPerpBankrun.ts +++ b/tests/placeAndMakeSwiftPerpBankrun.ts @@ -350,12 +350,12 @@ describe('place and make swift order', () => { // Switch the oracle over to using pyth lazer await makerDriftClient.initializePythLazerOracle(6); await makerDriftClient.postPythLazerOracleUpdate( - 6, + [6], PYTH_LAZER_HEX_STRING_SOL ); await makerDriftClient.postPythLazerOracleUpdate( - 6, + [6], PYTH_LAZER_HEX_STRING_SOL ); await makerDriftClient.updatePerpMarketOracle( @@ -452,7 +452,7 @@ describe('place and make swift order', () => { // Get pyth lazer instruction const pythLazerCrankIxs = makerDriftClient.getPostPythLazerOracleUpdateIxs( - 6, + [6], PYTH_LAZER_HEX_STRING_SOL, undefined, 1 diff --git a/tests/pythLazer.ts b/tests/pythLazer.ts index c5b832d0f..4fa255920 100644 --- a/tests/pythLazer.ts +++ b/tests/pythLazer.ts @@ -17,7 +17,11 @@ import { mockUSDCMint, } from './testHelpersLocalValidator'; import { Wallet, loadKeypair, EventSubscriber } from '../sdk/src'; -import { PYTH_LAZER_HEX_STRING_BTC } from './pythLazerData'; +import { + PYTH_LAZER_HEX_STRING_BTC, + PYTH_LAZER_HEX_STRING_MULTI, + PYTH_LAZER_HEX_STRING_SOL, +} from './pythLazerData'; describe('pyth lazer oracles', () => { const provider = anchor.AnchorProvider.local(undefined, { @@ -86,11 +90,13 @@ describe('pyth lazer oracles', () => { it('init feed', async () => { await driftClient.initializePythLazerOracle(1); + await driftClient.initializePythLazerOracle(2); + await driftClient.initializePythLazerOracle(6); }); it('crank', async () => { const ixs = await driftClient.getPostPythLazerOracleUpdateIxs( - 1, + [1], PYTH_LAZER_HEX_STRING_BTC ); @@ -104,4 +110,38 @@ describe('pyth lazer oracles', () => { console.log(simResult.value.logs); assert(simResult.value.err === null); }); + + it('crank multi', async () => { + const ixs = driftClient.getPostPythLazerOracleUpdateIxs( + [1, 2, 6], + PYTH_LAZER_HEX_STRING_MULTI + ); + + const message = new TransactionMessage({ + instructions: ixs, + payerKey: driftClient.wallet.payer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + }).compileToV0Message(); + const tx = new VersionedTransaction(message); + const simResult = await provider.connection.simulateTransaction(tx); + console.log(simResult.value.logs); + assert(simResult.value.err === null); + }); + + it('fails on wrong message passed', async () => { + const ixs = driftClient.getPostPythLazerOracleUpdateIxs( + [1], + PYTH_LAZER_HEX_STRING_SOL + ); + + const message = new TransactionMessage({ + instructions: ixs, + payerKey: driftClient.wallet.payer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + }).compileToV0Message(); + const tx = new VersionedTransaction(message); + const simResult = await provider.connection.simulateTransaction(tx); + console.log(simResult.value.logs); + assert(simResult.value.err !== null); + }); }); diff --git a/tests/pythLazerBankrun.ts b/tests/pythLazerBankrun.ts index 4e7466fae..7e1721bea 100644 --- a/tests/pythLazerBankrun.ts +++ b/tests/pythLazerBankrun.ts @@ -1,18 +1,28 @@ import * as anchor from '@coral-xyz/anchor'; import { Program } from '@coral-xyz/anchor'; import { + BN, OracleSource, + PEG_PRECISION, + PRICE_PRECISION, PTYH_LAZER_PROGRAM_ID, PYTH_LAZER_STORAGE_ACCOUNT_KEY, TestClient, + assert, getPythLazerOraclePublicKey, + isVariant, } from '../sdk/src'; import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; import { startAnchor } from 'solana-bankrun'; import { AccountInfo, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'; import { initializeQuoteSpotMarket, mockUSDCMint } from './testHelpers'; -import { PYTH_LAZER_HEX_STRING_BTC, PYTH_STORAGE_DATA } from './pythLazerData'; +import { + PYTH_LAZER_HEX_STRING_MULTI, + PYTH_LAZER_HEX_STRING_SOL, + PYTH_STORAGE_DATA, +} from './pythLazerData'; +import { mockOracleNoProgram } from './testHelpers'; // set up account infos to load into banks client const PYTH_STORAGE_ACCOUNT_INFO: AccountInfo = { @@ -71,7 +81,7 @@ describe('pyth pull oracles', () => { commitment: 'confirmed', }, activeSubAccountId: 0, - perpMarketIndexes: [], + perpMarketIndexes: [0], spotMarketIndexes: [0], subAccountIds: [], oracleInfos: [ @@ -89,6 +99,23 @@ describe('pyth pull oracles', () => { await driftClient.initialize(usdcMint.publicKey, true); await driftClient.subscribe(); + const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); + const ammInitialQuoteAssetReserve = new anchor.BN(10 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetReserve = new anchor.BN(10 * 10 ** 13).mul( + mantissaSqrtScale + ); + const periodicity = new BN(0); + await driftClient.initializePerpMarket( + 0, + await mockOracleNoProgram(bankrunContextWrapper, 224.3), + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(224 * PEG_PRECISION.toNumber()) + ); + await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); }); @@ -98,12 +125,30 @@ describe('pyth pull oracles', () => { it('init feed', async () => { await driftClient.initializePythLazerOracle(1); + await driftClient.initializePythLazerOracle(2); + await driftClient.initializePythLazerOracle(6); + }); + + it('crank single', async () => { + await driftClient.postPythLazerOracleUpdate([6], PYTH_LAZER_HEX_STRING_SOL); + await driftClient.updatePerpMarketOracle( + 0, + getPythLazerOraclePublicKey(driftClient.program.programId, 6), + OracleSource.PYTH_LAZER + ); + await driftClient.fetchAccounts(); + assert( + isVariant( + driftClient.getPerpMarketAccount(0).amm.oracleSource, + 'pythLazer' + ) + ); }); - it('crank', async () => { + it('crank multi', async () => { const tx = await driftClient.postPythLazerOracleUpdate( - 1, - PYTH_LAZER_HEX_STRING_BTC + [1, 2, 6], + PYTH_LAZER_HEX_STRING_MULTI ); console.log(tx); }); diff --git a/tests/pythLazerData.ts b/tests/pythLazerData.ts index ac18e8ce8..0867b35cb 100644 --- a/tests/pythLazerData.ts +++ b/tests/pythLazerData.ts @@ -5,3 +5,6 @@ export const PYTH_LAZER_HEX_STRING_BTC = export const PYTH_LAZER_HEX_STRING_SOL = 'b9011a8286e4219d980a176145b777aff894df914947c33855028f2b993a8963b131270c9cbcccd282685eb24bdeffcb8bec8c5203f6c882ad2e8b9adb0d1ba6f89b3e09f65210bee4fcf5b1cee1e537fabcfd95010297653b94af04d454fc473e94834f1c0075d3c79340e90513da2806000301060000000100fd09283605000000'; + +export const PYTH_LAZER_HEX_STRING_MULTI = + 'b9011a82bc4fe2aed6fb554f6724c98b49d0f3d7de89cb1ec95e536331dafd6da8db486610ac5d2f7d3e3156397f7f3f1adcf017b927c2f5b6128ea9b92362e20ee19a0df65210bee4fcf5b1cee1e537fabcfd95010297653b94af04d454fc473e94834f380075d3c79380f34c30172906000303010000000100d325584031090000020000000100d0cd67855b000000060000000100a026ea6505000000'; From a80b4c60a14b8dd479c83a57cf283a09fbbdec99 Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Thu, 12 Dec 2024 13:06:32 -0800 Subject: [PATCH 2/3] remove unnecessary annotation --- programs/drift/src/instructions/pyth_lazer_oracle.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/programs/drift/src/instructions/pyth_lazer_oracle.rs b/programs/drift/src/instructions/pyth_lazer_oracle.rs index bff7e2363..112cb553f 100644 --- a/programs/drift/src/instructions/pyth_lazer_oracle.rs +++ b/programs/drift/src/instructions/pyth_lazer_oracle.rs @@ -122,7 +122,6 @@ pub fn handle_update_pyth_lazer_oracle<'c: 'info, 'info>( } #[derive(Accounts)] -#[instruction(pyth_message: Vec)] pub struct UpdatePythLazerOracle<'info> { #[account(mut)] pub keeper: Signer<'info>, From 2457336e6ef6b164e0039016ee2febe91ece9986 Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Thu, 12 Dec 2024 13:22:19 -0800 Subject: [PATCH 3/3] default to 20bps for conf --- programs/drift/src/instructions/pyth_lazer_oracle.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/programs/drift/src/instructions/pyth_lazer_oracle.rs b/programs/drift/src/instructions/pyth_lazer_oracle.rs index 112cb553f..c2ea27d4d 100644 --- a/programs/drift/src/instructions/pyth_lazer_oracle.rs +++ b/programs/drift/src/instructions/pyth_lazer_oracle.rs @@ -89,8 +89,8 @@ pub fn handle_update_pyth_lazer_oracle<'c: 'info, 'info>( } } - // Default to 2% of the price for conf if bid > ask or one-sided market - let mut conf: i64 = price.0.get().safe_div(50)?; + // Default to 20bps of the price for conf if bid > ask or one-sided market + let mut conf: i64 = price.0.get().safe_div(500)?; if let (Some(bid), Some(ask)) = (best_bid_price, best_ask_price) { if bid.0.get() < ask.0.get() { conf = ask.0.get() - bid.0.get();