Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,432 changes: 1,129 additions & 303 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pinocchio-log = "0.5.1"
pinocchio-associated-token-account = "0.3.0"
wincode = { version = "0.4.5", features = ["derive"] }
pinocchio-pubkey = "0.3.0"
pyth-solana-receiver-sdk = "1.1.0"
Comment thread
AvhiMaz marked this conversation as resolved.

[dev-dependencies]
litesvm = "0.10.0"
Expand Down
17 changes: 17 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
pub const BPS: i64 = 10_000;
pub const T: u64 = 1; // fixed time horizon
pub const PRICE_SCALE: i64 = 1_000_000; // 6-decimal price scaling factor
pub const PRICE_ACC_LEN: usize = 134;

/// Byte offsets into a PriceUpdateV2 account (Anchor discriminator included)
pub const PRICE_ACC_OFFSET_FEED_ID: usize = 42;
pub const PRICE_ACC_OFFSET_PRICE: usize = 74;
pub const PRICE_ACC_OFFSET_CONF: usize = 82;
pub const PRICE_ACC_OFFSET_EXPONENT: usize = 90;
pub const PRICE_ACC_OFFSET_PUBLISH_TIME: usize = 94;

/// Maximum age of Pyth price in seconds before we reject it
pub const MAX_PRICE_AGE: i64 = 60;

/// ENYwebBThHzmzwPLAQvCucUTsjyfBSZdD9ViXksS4jPu
pub const FEED_ID: [u8; 32] = [
0xc6, 0xad, 0x3e, 0x84, 0x1d, 0x9c, 0x0f, 0x24, 0x8a, 0xdf, 0xf9, 0x0c, 0xf7, 0x76, 0xf8, 0x39,
0xfd, 0x59, 0xf1, 0xcb, 0xd8, 0xff, 0xbc, 0x8f, 0x94, 0x02, 0x88, 0x3e, 0xa1, 0x6e, 0x84, 0x20,
];
16 changes: 16 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use pinocchio::error::ProgramError;

#[derive(Debug)]
pub enum MakerError {
StalePythPrice,
InvalidFeedId,
UnauthorizedSigner,
ArithmeticOverflow,
InvalidAccountData,
}

impl From<MakerError> for ProgramError {
fn from(e: MakerError) -> Self {
ProgramError::Custom(e as u32)
}
}
13 changes: 10 additions & 3 deletions src/instructions/execute_swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ use pinocchio::{
AccountView, Address, ProgramResult,
cpi::{Seed, Signer},
error::ProgramError,
sysvars::{Sysvar, clock::Clock},
};
use pinocchio_token::instructions::Transfer;

use crate::{
constants::PRICE_SCALE, math::compute_price_spread, state::MakerConfig, utils::impl_load_ix,
constants::PRICE_SCALE,
math::{compute_price_spread, read_pyth_price},
state::MakerConfig,
utils::impl_load_ix,
};

#[repr(C)]
#[derive(Clone, Copy)]
pub struct SwapArgs {
pub amount_in: u64,
pub min_amount_out: u64,
pub mid_price: i64,
// pub mid_price: i64, we will be fetching from the oracle
pub side: u8,
pub _padding: [u8; 7],
}
Expand All @@ -33,6 +37,7 @@ pub fn process_execute_swap(
vault_base,
user_quote_ata,
vault_quote,
price_account,
Comment thread
AvhiMaz marked this conversation as resolved.
_token_program,
_remaining @ ..,
] = accounts
Expand All @@ -49,11 +54,13 @@ pub fn process_execute_swap(
if args.side > 1 {
return Err(ProgramError::InvalidInstructionData);
}
let clock = Clock::get()?;
let mid_price = read_pyth_price(price_account, clock.unix_timestamp)?;

let (bump, current_inventory, spread) = {
let config = MakerConfig::load(unsafe { maker_config.borrow_unchecked() })?;
let current_inventory = u64::from_le_bytes(config.current_inventory);
let spread = compute_price_spread(config, args.mid_price, current_inventory)?;
let spread = compute_price_spread(config, mid_price, current_inventory)?;
(config.bump, current_inventory, spread)
};

Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod entrypoint;

pub mod constants;
pub mod errors;
pub mod instructions;
pub mod math;
pub mod state;
Expand Down
74 changes: 72 additions & 2 deletions src/math.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
use crate::{constants::BPS, state::MakerConfig};
use pinocchio::error::ProgramError;
use crate::{
constants::{
BPS, FEED_ID, MAX_PRICE_AGE, PRICE_ACC_LEN, PRICE_ACC_OFFSET_CONF,
PRICE_ACC_OFFSET_EXPONENT, PRICE_ACC_OFFSET_FEED_ID, PRICE_ACC_OFFSET_PRICE,
PRICE_ACC_OFFSET_PUBLISH_TIME,
},
errors::MakerError,
state::MakerConfig,
};
use pinocchio::{AccountView, error::ProgramError};

pub struct PriceSpread {
pub reservation_price: i64,
Expand Down Expand Up @@ -88,3 +96,65 @@ pub fn compute_price_spread(
half_spread,
})
}

pub fn read_pyth_price(
price_account: &AccountView,
clock_unix_timestamp: i64,
) -> Result<i64, ProgramError> {
let data = price_account.try_borrow()?;

if data.len() < PRICE_ACC_LEN {
return Err(ProgramError::InvalidAccountData);
}

let feed_id = &data[PRICE_ACC_OFFSET_FEED_ID..PRICE_ACC_OFFSET_FEED_ID + 32];
if feed_id != FEED_ID {
return Err(MakerError::InvalidFeedId.into());
}

let raw_price = i64::from_le_bytes(
data[PRICE_ACC_OFFSET_PRICE..PRICE_ACC_OFFSET_PRICE + 8]
.try_into()
.map_err(|_| MakerError::InvalidAccountData)?,
);

let _ = u64::from_le_bytes(
data[PRICE_ACC_OFFSET_CONF..PRICE_ACC_OFFSET_CONF + 8]
.try_into()
.map_err(|_| MakerError::InvalidAccountData)?,
);

let exponent = i32::from_le_bytes(
data[PRICE_ACC_OFFSET_EXPONENT..PRICE_ACC_OFFSET_EXPONENT + 4]
.try_into()
.map_err(|_| MakerError::InvalidAccountData)?,
);

let publish_time = i64::from_le_bytes(
data[PRICE_ACC_OFFSET_PUBLISH_TIME..PRICE_ACC_OFFSET_PUBLISH_TIME + 8]
.try_into()
.map_err(|_| MakerError::InvalidAccountData)?,
);

if clock_unix_timestamp.saturating_sub(publish_time) > MAX_PRICE_AGE {
return Err(MakerError::StalePythPrice.into());
}
Comment thread
AvhiMaz marked this conversation as resolved.

let scale_exp: i32 = 6 + exponent; // target expo (6) + pyth expo

let normalized = if scale_exp >= 0 {
// multiply
let scale = 10i64.pow(scale_exp as u32);
raw_price
.checked_mul(scale)
.ok_or(ProgramError::ArithmeticOverflow)?
} else {
// divide
let scale = 10i64.pow((-scale_exp) as u32);
raw_price
.checked_div(scale)
.ok_or(ProgramError::ArithmeticOverflow)?
};

Ok(normalized)
}
56 changes: 47 additions & 9 deletions src/tests/swap.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::helpers::program_id;
use crate::{
constants::PRICE_SCALE,
constants::FEED_ID,
instructions::SwapArgs,
state::MakerConfig,
tests::helpers::{MarketSetup, setup_market_and_deposit},
Expand All @@ -14,7 +14,6 @@ use solana_signer::Signer;
use solana_transaction::Transaction;
use spl_token::solana_program::program_pack::Pack;

const MID_PRICE: i64 = PRICE_SCALE;
const DEPOSIT_AMOUNT: u64 = 2_000_000_000;

struct SwapSetup {
Expand All @@ -23,6 +22,39 @@ struct SwapSetup {
user_base_ata: solana_pubkey::Pubkey,
vault_quote: solana_pubkey::Pubkey,
user_quote_ata: solana_pubkey::Pubkey,
oracle: solana_pubkey::Pubkey,
}

fn create_mock_v2_oracle(
svm: &mut litesvm::LiteSVM,
price: i64,
expo: i32,
) -> solana_pubkey::Pubkey {
let oracle_pubkey = solana_pubkey::Pubkey::new_unique();
let mut data = vec![0u8; 150];

data[0..8].copy_from_slice(&[0x22, 0xf1, 0x23, 0x63, 0x9d, 0x7e, 0xf4, 0xcd]);
data[40] = 1;
data[42..74].copy_from_slice(&FEED_ID);
data[74..82].copy_from_slice(&price.to_le_bytes());
data[90..94].copy_from_slice(&expo.to_le_bytes());

let now: i64 = 1710000000;
data[94..102].copy_from_slice(&now.to_le_bytes());

svm.set_account(
oracle_pubkey,
solana_sdk::account::Account {
lamports: 1_000_000,
data,
owner: solana_pubkey::Pubkey::new_unique(),
executable: false,
rent_epoch: 0,
},
)
.unwrap();

oracle_pubkey
}

fn setup_swap() -> SwapSetup {
Expand Down Expand Up @@ -57,6 +89,8 @@ fn setup_swap() -> SwapSetup {
.send()
.unwrap();

let oracle = create_mock_v2_oracle(&mut inner.svm, 18_000_000_000, -8);

MintTo::new(
&mut inner.svm,
&inner.admin,
Expand Down Expand Up @@ -93,6 +127,7 @@ fn setup_swap() -> SwapSetup {
user_base_ata,
vault_quote,
user_quote_ata,
oracle,
}
}

Expand All @@ -114,6 +149,7 @@ fn make_swap_ix(setup: &SwapSetup, args: &SwapArgs) -> Instruction {
AccountMeta::new(setup.inner.vault, false),
AccountMeta::new(setup.user_quote_ata, false),
AccountMeta::new(setup.vault_quote, false),
AccountMeta::new_readonly(setup.oracle, false),
AccountMeta::new_readonly(TOKEN_PROGRAM_ID, false),
],
data,
Expand All @@ -123,11 +159,12 @@ fn make_swap_ix(setup: &SwapSetup, args: &SwapArgs) -> Instruction {
#[test]
fn test_buy_base() {
let mut setup = setup_swap();
let amount_in = 1_000_100_000u64;
let amount_in = 100_000_000u64;
let min_amount_out = 500_000u64;

let args = SwapArgs {
amount_in,
min_amount_out: 900_000_000,
mid_price: MID_PRICE,
min_amount_out,
side: 0,
_padding: [0u8; 7],
};
Expand Down Expand Up @@ -169,11 +206,12 @@ fn test_buy_base() {
#[test]
fn test_sell_base() {
let mut setup = setup_swap();
let amount_in = 1_000_000_000u64;
let amount_in = 1_000_000u64;
let min_amount_out = 175_000_000u64;

let args = SwapArgs {
amount_in,
min_amount_out: 900_000_000,
mid_price: MID_PRICE,
min_amount_out,
side: 1,
_padding: [0u8; 7],
};
Expand Down Expand Up @@ -221,7 +259,7 @@ fn test_swap_slippage_exceeded() {
let args = SwapArgs {
amount_in: 1_000_100_000,
min_amount_out: u64::MAX,
mid_price: MID_PRICE,
// mid_price: MID_PRICE,
side: 0,
_padding: [0u8; 7],
};
Expand Down
Loading