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

Feat/add price slot check (In on-chain programs) #16

Merged
merged 13 commits into from
Feb 17, 2022
15 changes: 15 additions & 0 deletions src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ pub enum PythClientInstruction {
///
/// No accounts required for this instruction
Noop,

PriceNotStale {
price_account_data: Vec<u8>
}
}

pub fn divide(numerator: PriceConf, denominator: PriceConf) -> Instruction {
Expand Down Expand Up @@ -94,3 +98,14 @@ pub fn noop() -> Instruction {
data: PythClientInstruction::Noop.try_to_vec().unwrap(),
}
}

// Returns ok if price is not stale
ali-bahjati marked this conversation as resolved.
Show resolved Hide resolved
pub fn price_not_stale(price_account_data: Vec<u8>) -> Instruction {
ali-bahjati marked this conversation as resolved.
Show resolved Hide resolved
Instruction {
program_id: id(),
accounts: vec![],
data: PythClientInstruction::PriceNotStale { price_account_data }
.try_to_vec()
.unwrap(),
}
}
38 changes: 28 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ use bytemuck::{
Pod, PodCastError, Zeroable,
};

#[cfg(target_arch = "bpf")]
use solana_program::{clock::Clock, sysvar::Sysvar};

solana_program::declare_id!("PythC11111111111111111111111111111111111111");

pub const MAGIC : u32 = 0xa1b2c3d4;
pub const VERSION_2 : u32 = 2;
pub const VERSION : u32 = VERSION_2;
pub const MAP_TABLE_SIZE : usize = 640;
pub const PROD_ACCT_SIZE : usize = 512;
pub const PROD_HDR_SIZE : usize = 48;
pub const PROD_ATTR_SIZE : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE;
pub const MAGIC : u32 = 0xa1b2c3d4;
pub const VERSION_2 : u32 = 2;
pub const VERSION : u32 = VERSION_2;
pub const MAP_TABLE_SIZE : usize = 640;
pub const PROD_ACCT_SIZE : usize = 512;
pub const PROD_HDR_SIZE : usize = 48;
pub const PROD_ATTR_SIZE : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE;
pub const MAX_SLOT_DIFFERENCE : u64 = 25;

/// The type of Pyth account determines what data it contains
#[derive(Copy, Clone)]
Expand Down Expand Up @@ -180,9 +184,9 @@ pub struct Ema
/// The current value of the EMA
pub val : i64,
/// numerator state for next update
numer : i64,
pub numer : i64,
/// denominator state for next update
denom : i64
pub denom : i64
}

/// Price accounts represent a continuously-updating price feed for a product.
Expand Down Expand Up @@ -243,13 +247,26 @@ unsafe impl Zeroable for Price {}
unsafe impl Pod for Price {}

impl Price {
/**
* Get the current status of the aggregate price.
* If this lib is used on-chain it will mark price status as unknown if price has not been updated for a while.
*/
pub fn get_current_status(&self) -> PriceStatus {
ali-bahjati marked this conversation as resolved.
Show resolved Hide resolved
#[cfg(target_arch = "bpf")]
if matches!(self.agg.status, PriceStatus::Trading) &&
Clock::get().unwrap().slot - self.agg.pub_slot > MAX_SEND_LATENCY {
return PriceStatus::Unknown;
}
self.agg.status
}

/**
* Get the current price and confidence interval as fixed-point numbers of the form a * 10^e.
* Returns a struct containing the current price, confidence interval, and the exponent for both
* numbers. Returns `None` if price information is currently unavailable for any reason.
*/
pub fn get_current_price(&self) -> Option<PriceConf> {
if !matches!(self.agg.status, PriceStatus::Trading) {
if !matches!(self.get_current_status(), PriceStatus::Trading) {
None
} else {
Some(PriceConf {
Expand Down Expand Up @@ -394,6 +411,7 @@ pub fn load_price(data: &[u8]) -> Result<&Price, PythError> {
return Ok(pyth_price);
}


pub struct AttributeIter<'a> {
attrs: &'a [u8],
}
Expand Down
13 changes: 12 additions & 1 deletion src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
pubkey::Pubkey,
program_error::ProgramError
};

use crate::{
instruction::PythClientInstruction,
instruction::PythClientInstruction, load_price, PriceStatus,
};

pub fn process_instruction(
Expand Down Expand Up @@ -41,5 +42,15 @@ pub fn process_instruction(
PythClientInstruction::Noop => {
Ok(())
}
PythClientInstruction::PriceNotStale { price_account_data } => {
let price = load_price(&price_account_data[..])?;

match price.get_current_status() {
PriceStatus::Trading => {
Ok(())
}
_ => Err(ProgramError::Custom(0))
}
}
}
}
94 changes: 94 additions & 0 deletions tests/stale_price.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#![cfg(feature = "test-bpf")] // This only runs on bpf

use {
bytemuck::bytes_of,
pyth_client::{id, MAGIC, VERSION_2, instruction, PriceType, Price, AccountType, AccKey, Ema, PriceComp, PriceInfo, CorpAction, PriceStatus},
pyth_client::processor::process_instruction,
solana_program::instruction::Instruction,
solana_program_test::*,
solana_sdk::{signature::Signer, transaction::Transaction, transport::TransportError},
};

async fn test_instr(instr: Instruction) -> Result<(), TransportError> {
ali-bahjati marked this conversation as resolved.
Show resolved Hide resolved
let (mut banks_client, payer, recent_blockhash) = ProgramTest::new(
"pyth_client",
id(),
processor!(process_instruction),
)
.start()
.await;
let mut transaction = Transaction::new_with_payer(
&[instr],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer], recent_blockhash);
banks_client.process_transaction(transaction).await
}

fn price_all_zero() -> Price {
let acc_key = AccKey {
val: [0; 32]
};

let ema = Ema {
val: 0,
numer: 0,
denom: 0
};

let price_info = PriceInfo {
conf: 0,
corp_act: CorpAction::NoCorpAct,
price: 0,
pub_slot: 0,
status: PriceStatus::Unknown
};

let price_comp = PriceComp {
agg: price_info,
latest: price_info,
publisher: acc_key
};

Price {
magic: MAGIC,
ver: VERSION_2,
atype: AccountType::Price as u32,
size: 0,
ptype: PriceType::Price,
expo: 0,
num: 0,
num_qt: 0,
last_slot: 0,
valid_slot: 0,
twap: ema,
twac: ema,
drv1: 0,
drv2: 0,
prod: acc_key,
next: acc_key,
prev_slot: 0,
prev_price: 0,
prev_conf: 0,
drv3: 0,
agg: price_info,
comp: [price_comp; 32]
}
}


#[tokio::test]
async fn test_price_not_stale() {
let mut price = price_all_zero();
price.agg.status = PriceStatus::Trading;
test_instr(instruction::price_not_stale(bytes_of(&price).to_vec())).await.unwrap();
}


#[tokio::test]
async fn test_price_stale() {
let mut price = price_all_zero();
price.agg.status = PriceStatus::Trading;
price.agg.pub_slot = 100; // It will cause an overflow because this is bigger than Solana slot which is impossible in reality
ali-bahjati marked this conversation as resolved.
Show resolved Hide resolved
test_instr(instruction::price_not_stale(bytes_of(&price).to_vec())).await.unwrap_err();
}