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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ println!("price: ({} +- {}) x 10^{}", price.price, price.conf, price.expo);

The price is returned along with a confidence interval that represents the degree of uncertainty in the price.
Both values are represented as fixed-point numbers, `a * 10^e`.
The method will return `None` if the price is not currently available.
The method will return `None` if the price is not currently available based on the price status. Get the current price status:
ali-bahjati marked this conversation as resolved.
Show resolved Hide resolved

```rust
let price_status: PriceStatus = price_account.get_current_status();
```

### Non-USD prices

Expand Down
20 changes: 20 additions & 0 deletions src/instruction.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Program instructions for end-to-end testing and instruction counts

use crate::PriceStatus;

use {
crate::id,
borsh::{BorshDeserialize, BorshSerialize},
Expand Down Expand Up @@ -34,6 +36,13 @@ pub enum PythClientInstruction {
///
/// No accounts required for this instruction
Noop,

PriceStatusCheck {
// Adding Borsh SerDe can be expensive (Price Account is big) and requires to add Borsh SerDe, Debug, Default to all structs
// (also in enum Default is experimental which we should provide in another way). I think it's best to left structs intact for this purpose.
price_account_data: Vec<u8>,
ali-bahjati marked this conversation as resolved.
Show resolved Hide resolved
expected_price_status: PriceStatus
}
}

pub fn divide(numerator: PriceConf, denominator: PriceConf) -> Instruction {
Expand Down Expand Up @@ -94,3 +103,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_status_check(price_account_data: Vec<u8>, expected_price_status: PriceStatus) -> Instruction {
ali-bahjati marked this conversation as resolved.
Show resolved Hide resolved
Instruction {
program_id: id(),
accounts: vec![],
data: PythClientInstruction::PriceStatusCheck { price_account_data, expected_price_status }
.try_to_vec()
.unwrap(),
}
}
51 changes: 37 additions & 14 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,25 @@ pub mod processor;
pub mod instruction;

use std::mem::size_of;
use borsh::{BorshSerialize, BorshDeserialize};
use bytemuck::{
cast_slice, from_bytes, try_cast_slice,
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 All @@ -40,7 +45,7 @@ pub enum AccountType
}

/// The current status of a price feed.
#[derive(Copy, Clone, PartialEq)]
#[derive(Copy, Clone, PartialEq, BorshSerialize, BorshDeserialize, Debug)]
Reisen marked this conversation as resolved.
Show resolved Hide resolved
#[repr(C)]
pub enum PriceStatus
{
Expand Down Expand Up @@ -146,11 +151,15 @@ unsafe impl Pod for Product {}
#[repr(C)]
pub struct PriceInfo
{
/// the current price
/// the current price.
/// For the aggregate price use price.get_current_price() whenever possible. It has more checks to make sure price is valid.
pub price : i64,
/// confidence interval around the price
/// confidence interval around the price.
/// For the aggregate confidence use price.get_current_price() whenever possible. It has more checks to make sure price is valid.
pub conf : u64,
/// status of price (Trading is valid)
/// status of price (Trading is valid).
/// For the aggregate status use price.get_current_status() whenever possible.
/// Price data can sometimes go stale and the function handles the status in such cases.
ali-bahjati marked this conversation as resolved.
Show resolved Hide resolved
pub status : PriceStatus,
/// notification of any corporate action
pub corp_act : CorpAction,
Expand Down Expand Up @@ -180,9 +189,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 +252,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_SLOT_DIFFERENCE {
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 +416,7 @@ pub fn load_price(data: &[u8]) -> Result<&Price, PythError> {
return Ok(pyth_price);
}


pub struct AttributeIter<'a> {
attrs: &'a [u8],
}
Expand Down
12 changes: 11 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,
};

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

if price.get_current_status() == expected_price_status {
Ok(())
} else {
Err(ProgramError::Custom(0))
}
}
}
}
23 changes: 23 additions & 0 deletions tests/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use {
pyth_client::id,
pyth_client::processor::process_instruction,
solana_program::instruction::Instruction,
solana_program_test::*,
solana_sdk::{signature::Signer, transaction::Transaction},
};

pub async fn test_instr(instr: Instruction) {
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.unwrap()
}
22 changes: 3 additions & 19 deletions tests/instruction_count.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,10 @@
use {
pyth_client::{id, instruction, PriceConf},
pyth_client::processor::process_instruction,
solana_program::instruction::Instruction,
pyth_client::{instruction, PriceConf},
solana_program_test::*,
solana_sdk::{signature::Signer, transaction::Transaction},
};

async fn test_instr(instr: Instruction) {
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.unwrap();
}
mod common;
use common::test_instr;

fn pc(price: i64, conf: u64, expo: i32) -> PriceConf {
PriceConf {
Expand Down
82 changes: 82 additions & 0 deletions tests/stale_price.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#![cfg(feature = "test-bpf")] // This only runs on bpf

use {
bytemuck::bytes_of,
pyth_client::{MAGIC, VERSION_2, instruction, PriceType, Price, AccountType, AccKey, Ema, PriceComp, PriceInfo, CorpAction, PriceStatus},
solana_program_test::*,
};


mod common;
use common::test_instr;

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_status_check(bytes_of(&price).to_vec(), PriceStatus::Trading)).await;
}


#[tokio::test]
async fn test_price_stale() {
let mut price = price_all_zero();
price.agg.status = PriceStatus::Trading;
// Value 100 will cause an overflow because this is bigger than Solana slot in the test suite (its ~1-5).
// As the check will be 5u - 100u ~= 1e18 > MAX_SLOT_DIFFERENCE. It can only break when Solana slot in the test suite becomes
// between 100 and 100+MAX_SLOT_DIFFERENCE.
price.agg.pub_slot = 100;
test_instr(instruction::price_status_check(bytes_of(&price).to_vec(), PriceStatus::Unknown)).await;
}