Skip to content
This repository was archived by the owner on Jun 30, 2022. It is now read-only.

Commit 2f8fd3f

Browse files
committed
Add slot check in load_price for bpf arch (on-chain programs)
- Introduces another Price struct which doesn't have comps (published prices) and unused fields. The reason is that we want to mutate struct (to add the check) and Price Account Data has ~3kb so copying is expensive. This creates a significantly smaller one. This naming allow us to have least impact on existing consumers. The price account data is available as price account data if a consumer needs it. - Also make Ema fields public
1 parent 550fc71 commit 2f8fd3f

File tree

4 files changed

+218
-18
lines changed

4 files changed

+218
-18
lines changed

src/instruction.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ pub enum PythClientInstruction {
3434
///
3535
/// No accounts required for this instruction
3636
Noop,
37+
38+
PriceNotStale {
39+
price_account_data: Vec<u8>
40+
}
3741
}
3842

3943
pub fn divide(numerator: PriceConf, denominator: PriceConf) -> Instruction {
@@ -94,3 +98,14 @@ pub fn noop() -> Instruction {
9498
data: PythClientInstruction::Noop.try_to_vec().unwrap(),
9599
}
96100
}
101+
102+
// Returns ok if price is not stale
103+
pub fn price_not_stale(price_account_data: Vec<u8>) -> Instruction {
104+
Instruction {
105+
program_id: id(),
106+
accounts: vec![],
107+
data: PythClientInstruction::PriceNotStale { price_account_data }
108+
.try_to_vec()
109+
.unwrap(),
110+
}
111+
}

src/lib.rs

Lines changed: 97 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,19 @@ use bytemuck::{
1818
Pod, PodCastError, Zeroable,
1919
};
2020

21+
#[cfg(target_arch = "bpf")]
22+
use solana_program::{clock::Clock, sysvar::Sysvar};
23+
2124
solana_program::declare_id!("PythC11111111111111111111111111111111111111");
2225

23-
pub const MAGIC : u32 = 0xa1b2c3d4;
24-
pub const VERSION_2 : u32 = 2;
25-
pub const VERSION : u32 = VERSION_2;
26-
pub const MAP_TABLE_SIZE : usize = 640;
27-
pub const PROD_ACCT_SIZE : usize = 512;
28-
pub const PROD_HDR_SIZE : usize = 48;
29-
pub const PROD_ATTR_SIZE : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE;
26+
pub const MAGIC : u32 = 0xa1b2c3d4;
27+
pub const VERSION_2 : u32 = 2;
28+
pub const VERSION : u32 = VERSION_2;
29+
pub const MAP_TABLE_SIZE : usize = 640;
30+
pub const PROD_ACCT_SIZE : usize = 512;
31+
pub const PROD_HDR_SIZE : usize = 48;
32+
pub const PROD_ATTR_SIZE : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE;
33+
pub const MAX_SEND_LATENCY : u64 = 25;
3034

3135
/// The type of Pyth account determines what data it contains
3236
#[derive(Copy, Clone)]
@@ -180,15 +184,15 @@ pub struct Ema
180184
/// The current value of the EMA
181185
pub val : i64,
182186
/// numerator state for next update
183-
numer : i64,
187+
pub numer : i64,
184188
/// denominator state for next update
185-
denom : i64
189+
pub denom : i64
186190
}
187191

188192
/// Price accounts represent a continuously-updating price feed for a product.
189193
#[derive(Copy, Clone)]
190194
#[repr(C)]
191-
pub struct Price
195+
pub struct PriceAccountData
192196
{
193197
/// pyth magic number
194198
pub magic : u32,
@@ -235,14 +239,71 @@ pub struct Price
235239
/// price components one per quoter
236240
pub comp : [PriceComp;32]
237241
}
242+
#[derive(Copy, Clone)]
243+
#[repr(C)]
244+
pub struct Price {
245+
/// account type
246+
pub atype : u32,
247+
/// price account size
248+
pub size : u32,
249+
/// price or calculation type
250+
pub ptype : PriceType,
251+
/// price exponent
252+
pub expo : i32,
253+
/// number of component prices
254+
pub num : u32,
255+
/// number of quoters that make up aggregate
256+
pub num_qt : u32,
257+
/// slot of last valid (not unknown) aggregate price
258+
pub last_slot : u64,
259+
/// valid slot-time of agg. price
260+
pub valid_slot : u64,
261+
/// time-weighted average price
262+
pub twap : Ema,
263+
/// time-weighted average confidence interval
264+
pub twac : Ema,
265+
/// product account key
266+
pub prod : AccKey,
267+
/// next Price account in linked list
268+
pub next : AccKey,
269+
/// valid slot of previous update
270+
pub prev_slot : u64,
271+
/// aggregate price of previous update
272+
pub prev_price : i64,
273+
/// confidence interval of previous update
274+
pub prev_conf : u64,
275+
/// aggregate price info
276+
pub agg : PriceInfo,
277+
}
238278

239279
#[cfg(target_endian = "little")]
240-
unsafe impl Zeroable for Price {}
280+
unsafe impl Zeroable for PriceAccountData {}
241281

242282
#[cfg(target_endian = "little")]
243-
unsafe impl Pod for Price {}
283+
unsafe impl Pod for PriceAccountData {}
244284

245285
impl Price {
286+
fn from_price_account_data(price_account_data: &PriceAccountData) -> Self {
287+
Price {
288+
atype: price_account_data.atype,
289+
size: price_account_data.size,
290+
ptype: price_account_data.ptype,
291+
expo: price_account_data.expo,
292+
num: price_account_data.num,
293+
num_qt: price_account_data.num_qt,
294+
last_slot: price_account_data.last_slot,
295+
valid_slot: price_account_data.valid_slot,
296+
twap: price_account_data.twap,
297+
twac: price_account_data.twac,
298+
prod: price_account_data.prod,
299+
next: price_account_data.next,
300+
prev_slot: price_account_data.prev_slot,
301+
prev_price: price_account_data.prev_price,
302+
prev_conf: price_account_data.prev_conf,
303+
agg: price_account_data.agg
304+
}
305+
}
306+
246307
/**
247308
* Get the current price and confidence interval as fixed-point numbers of the form a * 10^e.
248309
* Returns a struct containing the current price, confidence interval, and the exponent for both
@@ -378,19 +439,38 @@ pub fn load_product(data: &[u8]) -> Result<&Product, PythError> {
378439
}
379440

380441
/** Get a `Price` account from the raw byte value of a Solana account. */
381-
pub fn load_price(data: &[u8]) -> Result<&Price, PythError> {
382-
let pyth_price = load::<Price>(&data).map_err(|_| PythError::InvalidAccountData)?;
442+
pub fn load_price_account_data(data: &[u8]) -> Result<&PriceAccountData, PythError> {
443+
let price_account_data = load::<PriceAccountData>(&data).map_err(|_| PythError::InvalidAccountData)?;
383444

384-
if pyth_price.magic != MAGIC {
445+
if price_account_data.magic != MAGIC {
385446
return Err(PythError::InvalidAccountData);
386447
}
387-
if pyth_price.ver != VERSION_2 {
448+
if price_account_data.ver != VERSION_2 {
388449
return Err(PythError::BadVersionNumber);
389450
}
390-
if pyth_price.atype != AccountType::Price as u32 {
451+
if price_account_data.atype != AccountType::Price as u32 {
391452
return Err(PythError::WrongAccountType);
392453
}
393454

455+
return Ok(price_account_data);
456+
}
457+
458+
/** Get a modified `Price` struct from the raw byte value of a Solana Price account.
459+
* If used on-chain it will update the status to unknown if price is not updated for a long time.
460+
*/
461+
pub fn load_price(data: &[u8]) -> Result<Price, PythError> {
462+
let price_account_data = load_price_account_data(data)?;
463+
464+
#[allow(unused_mut)]
465+
let mut pyth_price = Price::from_price_account_data(price_account_data);
466+
467+
#[cfg(target_arch = "bpf")]
468+
if let PriceStatus::Trading = pyth_price.agg.status {
469+
if Clock::get().unwrap().slot - pyth_price.agg.pub_slot > MAX_SEND_LATENCY {
470+
pyth_price.agg.status = PriceStatus::Unknown;
471+
}
472+
}
473+
394474
return Ok(pyth_price);
395475
}
396476

src/processor.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ use solana_program::{
55
account_info::AccountInfo,
66
entrypoint::ProgramResult,
77
pubkey::Pubkey,
8+
program_error::ProgramError
89
};
910

1011
use crate::{
11-
instruction::PythClientInstruction,
12+
instruction::PythClientInstruction, load_price, PriceStatus,
1213
};
1314

1415
pub fn process_instruction(
@@ -41,5 +42,15 @@ pub fn process_instruction(
4142
PythClientInstruction::Noop => {
4243
Ok(())
4344
}
45+
PythClientInstruction::PriceNotStale { price_account_data } => {
46+
let price = load_price(&price_account_data[..])?;
47+
48+
match price.agg.status {
49+
PriceStatus::Trading => {
50+
Ok(())
51+
}
52+
_ => Err(ProgramError::Custom(0))
53+
}
54+
}
4455
}
4556
}

tests/stale_price.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#![cfg(feature = "test-bpf")] // This only runs on bpf
2+
3+
use {
4+
bytemuck::bytes_of,
5+
pyth_client::{id, MAGIC, VERSION_2, instruction, PriceType, PriceAccountData, AccountType, AccKey, Ema, PriceComp, PriceInfo, CorpAction, PriceStatus},
6+
pyth_client::processor::process_instruction,
7+
solana_program::instruction::Instruction,
8+
solana_program_test::*,
9+
solana_sdk::{signature::Signer, transaction::Transaction, transport::TransportError},
10+
};
11+
12+
async fn test_instr(instr: Instruction) -> Result<(), TransportError> {
13+
let (mut banks_client, payer, recent_blockhash) = ProgramTest::new(
14+
"pyth_client",
15+
id(),
16+
processor!(process_instruction),
17+
)
18+
.start()
19+
.await;
20+
let mut transaction = Transaction::new_with_payer(
21+
&[instr],
22+
Some(&payer.pubkey()),
23+
);
24+
transaction.sign(&[&payer], recent_blockhash);
25+
banks_client.process_transaction(transaction).await
26+
}
27+
28+
fn price_all_zero() -> PriceAccountData {
29+
let acc_key = AccKey {
30+
val: [0; 32]
31+
};
32+
33+
let ema = Ema {
34+
val: 0,
35+
numer: 0,
36+
denom: 0
37+
};
38+
39+
let price_info = PriceInfo {
40+
conf: 0,
41+
corp_act: CorpAction::NoCorpAct,
42+
price: 0,
43+
pub_slot: 0,
44+
status: PriceStatus::Unknown
45+
};
46+
47+
let price_comp = PriceComp {
48+
agg: price_info,
49+
latest: price_info,
50+
publisher: acc_key
51+
};
52+
53+
PriceAccountData {
54+
magic: MAGIC,
55+
ver: VERSION_2,
56+
atype: AccountType::Price as u32,
57+
size: 0,
58+
ptype: PriceType::Price,
59+
expo: 0,
60+
num: 0,
61+
num_qt: 0,
62+
last_slot: 0,
63+
valid_slot: 0,
64+
twap: ema,
65+
twac: ema,
66+
drv1: 0,
67+
drv2: 0,
68+
prod: acc_key,
69+
next: acc_key,
70+
prev_slot: 0,
71+
prev_price: 0,
72+
prev_conf: 0,
73+
drv3: 0,
74+
agg: price_info,
75+
comp: [price_comp; 32]
76+
}
77+
}
78+
79+
80+
#[tokio::test]
81+
async fn test_price_not_stale() {
82+
let mut price = price_all_zero();
83+
price.agg.status = PriceStatus::Trading;
84+
test_instr(instruction::price_not_stale(bytes_of(&price).to_vec())).await.unwrap();
85+
}
86+
87+
88+
#[tokio::test]
89+
async fn test_price_stale() {
90+
let mut price = price_all_zero();
91+
price.agg.status = PriceStatus::Trading;
92+
price.agg.pub_slot = 100; // It will cause an overflow because this is bigger than Solana slot which is impossible in reality
93+
test_instr(instruction::price_not_stale(bytes_of(&price).to_vec())).await.unwrap_err();
94+
}

0 commit comments

Comments
 (0)