Skip to content

Commit

Permalink
Add ffi perp order simulation (#76)
Browse files Browse the repository at this point in the history
* Add ffi simulate_place_perp_orders to check local order sim for ok/err
* update libdrift-ffi-sys v2.100.1
  • Loading branch information
jordy25519 authored Nov 21, 2024
1 parent 3da0979 commit 5ce0969
Show file tree
Hide file tree
Showing 11 changed files with 367 additions and 186 deletions.
22 changes: 6 additions & 16 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ on:
- "**.toml"
- "**.lock"
- ".github/workflows/*.yml"

jobs:
format-build-test:
env:
CARGO_DRIFT_FFI_PATH: /usr/lib
runs-on: ubicloud
steps:
- name: Check out
Expand All @@ -32,32 +34,20 @@ jobs:
rustup component add clippy rustfmt
- name: install libdrift_ffi_sys
run: |
curl -L https://github.com/user-attachments/files/17806677/libdrift_ffi_sys.so.zip > ffi.zip
curl -L https://github.com/user-attachments/files/17849111/libdrift_ffi_sys.so.zip > ffi.zip
unzip ffi.zip
ldd libdrift_ffi_sys.so
sudo cp libdrift_ffi_sys.so /usr/lib
sudo cp libdrift_ffi_sys.so $CARGO_DRIFT_FFI_PATH
ldconfig -p
- name: Format
run: cargo fmt --all -- --check
- name: Build
run: cargo check
env:
CARGO_DRIFT_FFI_PATH: "/usr/lib"
# - name: Clippy
# uses: giraffate/clippy-action@v1
# with:
# reporter: 'github-pr-review'
# github_token: ${{ secrets.GITHUB_TOKEN }}
# env:
# RUST_TOOLCHAIN: stable-x86_64-linux-unknown-gnu # force clippy to build with same rust version
# CARGO_DRIFT_FFI_PATH: "/usr/lib"
- name: Test
run: |
cargo test --no-fail-fast --lib -- --nocapture
cargo test --no-fail-fast --test integration -- --nocapture --test-threads 1
env:
RUST_LOG: info
TEST_DEVNET_RPC_ENDPOINT: ${{ secrets.DEVNET_RPC_ENDPOINT }}
TEST_MAINNET_RPC_ENDPOINT: ${{ secrets.MAINNET_RPC_ENDPOINT }}
TEST_PRIVATE_KEY: ${{ secrets.TEST_PRIVATE_KEY }}
CARGO_DRIFT_FFI_PATH: "/usr/lib"
TEST_PRIVATE_KEY: ${{ secrets.TEST_PRIVATE_KEY }}
2 changes: 1 addition & 1 deletion crates/drift-ffi-sys
Submodule drift-ffi-sys updated 2 files
+91 −2 src/exports.rs
+30 −2 src/types.rs
6 changes: 4 additions & 2 deletions crates/src/drift_idl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
#![doc = r""]
#![doc = r" Auto-generated IDL types, manual edits do not persist (see `crates/drift-idl-gen`)"]
#![doc = r""]
use self::traits::ToAccountMetas;
use anchor_lang::{
prelude::{
account,
Expand All @@ -13,6 +12,8 @@ use anchor_lang::{
};
use serde::{Deserialize, Serialize};
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};

use self::traits::ToAccountMetas;
pub mod traits {
use solana_sdk::instruction::AccountMeta;
#[doc = r" This is distinct from the anchor_lang version of the trait"]
Expand Down Expand Up @@ -1918,8 +1919,9 @@ pub mod instructions {
}
pub mod types {
#![doc = r" IDL types"]
use super::*;
use std::ops::Mul;

use super::*;
#[doc = ""]
#[doc = " backwards compatible u128 deserializing data from rust <=1.76.0 when u/i128 was 8-byte aligned"]
#[doc = " https://solana.stackexchange.com/questions/7720/using-u128-without-sacrificing-alignment-8"]
Expand Down
218 changes: 199 additions & 19 deletions crates/src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ extern "C" {
user: &accounts::User,
market_index: u16,
) -> FfiResult<&types::PerpPosition>;
#[allow(improper_ctypes)]
pub fn orders_place_perp_order(
user: &accounts::User,
state: &accounts::State,
order_params: &types::OrderParams,
accounts: &mut AccountsList,
) -> FfiResult<bool>;
}

//
Expand Down Expand Up @@ -159,6 +166,20 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info(
to_sdk_result(res)
}

/// Simulates the program's `place_perp_order` ix
/// Useful to verify an order can be placed given factors such as available margin, etc.
///
/// Returns `true` if the order could be placed
pub fn simulate_place_perp_order(
user: &accounts::User,
accounts: &mut AccountsList,
state: &accounts::State,
order_params: &types::OrderParams,
) -> SdkResult<bool> {
let res = unsafe { orders_place_perp_order(user, state, order_params, accounts) };
to_sdk_result(res)
}

impl types::SpotPosition {
pub fn is_available(&self) -> bool {
unsafe { spot_position_is_available(self) }
Expand Down Expand Up @@ -381,33 +402,35 @@ mod tests {
use solana_sdk::{account::Account, pubkey::Pubkey};
use type_layout::TypeLayout;

use super::{AccountWithKey, AccountsList, MarginCalculation, MarginContextMode};
use super::{
simulate_place_perp_order, AccountWithKey, AccountsList, MarginCalculation,
MarginContextMode,
};
use crate::{
constants::{self},
accounts::State,
constants::{self, ids::pyth_program},
create_account_info,
drift_idl::{
accounts::{PerpMarket, SpotMarket, User},
types::{
ContractType, MarginRequirementType, OracleSource, Order, OrderType, PerpPosition,
SpotBalanceType, SpotPosition,
ContractType, MarginRequirementType, OracleSource, Order, OrderParams, OrderType,
PerpPosition, SpotBalanceType, SpotPosition,
},
},
ffi::{
calculate_auction_price,
calculate_margin_requirement_and_total_collateral_and_liability_info, get_oracle_price,
},
math::constants::{
BASE_PRECISION_I64, LIQUIDATION_FEE_PRECISION, MARGIN_PRECISION, PRICE_PRECISION_I64,
QUOTE_PRECISION, QUOTE_PRECISION_I64, SPOT_BALANCE_PRECISION,
BASE_PRECISION, BASE_PRECISION_I64, LIQUIDATION_FEE_PRECISION, MARGIN_PRECISION,
PRICE_PRECISION_I64, QUOTE_PRECISION, QUOTE_PRECISION_I64, SPOT_BALANCE_PRECISION,
SPOT_BALANCE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION,
},
PositionDirection,
types::MarketType,
utils::test_utils::{get_account_bytes, get_pyth_price},
HistoricalOracleData, MarketStatus, PositionDirection, AMM,
};

const _SOL_PYTH_PRICE_STR: &str = include_str!("../../res/sol-oracle-pyth.hex");
/// encoded pyth price account for SOL, see math/liquidation.rs tests
const SOL_PYTH_PRICE: std::cell::LazyCell<Vec<u8>> =
std::cell::LazyCell::new(|| hex::decode(_SOL_PYTH_PRICE_STR).unwrap());

fn sol_spot_market() -> SpotMarket {
SpotMarket {
market_index: 1,
Expand All @@ -422,15 +445,42 @@ mod tests {
maintenance_liability_weight: 11 * SPOT_WEIGHT_PRECISION / 10,
liquidator_fee: LIQUIDATION_FEE_PRECISION / 1000,
deposit_balance: (1_000 * SPOT_BALANCE_PRECISION).into(),
order_step_size: 1_000,
order_tick_size: 1_000,
historical_oracle_data: HistoricalOracleData {
last_oracle_price_twap5min: 240_000_000_000,
..Default::default()
},
..Default::default()
}
}

fn usdc_spot_market() -> SpotMarket {
SpotMarket {
market_index: 0,
oracle_source: OracleSource::QuoteAsset,
cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION.into(),
decimals: 6,
initial_asset_weight: SPOT_WEIGHT_PRECISION,
maintenance_asset_weight: SPOT_WEIGHT_PRECISION,
deposit_balance: (100_000 * SPOT_BALANCE_PRECISION).into(),
liquidator_fee: 0,
order_step_size: 1_000,
order_tick_size: 1_000,
historical_oracle_data: HistoricalOracleData {
last_oracle_price_twap5min: 1_000_000,
..Default::default()
},
..SpotMarket::default()
}
}

#[test]
fn ffi_deser_1_76_0_spot_market() {
// smoke test for deserializing program data (where u128/i128 alignment is 8)
let buf = hex_literal::hex!("64b1086ba84141270000000000000000000000000000000000000000000000000000000000000000fe650f0367d4a7ef9815a593ea15d36593f0643aaaf0149bb04be67ab851decd0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010a5d4e800000000000000000000000000000000000000000000000000000000e40b5402000000000000000000000000e40b54020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000401f000028230000e02e0000f82a000000000000e803000000000000000000000000000000000000090000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
let actual: &SpotMarket = bytemuck::from_bytes::<SpotMarket>(&buf.as_ref()[8..]); // ignore dscriminator
let spot_market_borsh =
hex::decode(include_str!("../../res/spot_market_1_76_0.hex")).unwrap();
let actual: &SpotMarket = bytemuck::from_bytes::<SpotMarket>(&spot_market_borsh[8..]); // ignore dscriminator

assert_eq!(actual, &sol_spot_market());
}
Expand Down Expand Up @@ -605,8 +655,7 @@ mod tests {
fn ffi_get_oracle_price() {
let oracle_pubkey = Pubkey::new_unique();
let oracle_account = Account {
// encoded from pyth Price, see liquidation tests
data: SOL_PYTH_PRICE.clone(),
data: get_account_bytes(&mut get_pyth_price(240, 9)).to_vec(),
owner: constants::ids::pyth_program::ID,
..Default::default()
};
Expand All @@ -621,7 +670,7 @@ mod tests {
let oracle_price_data = result.unwrap();

dbg!(oracle_price_data.price);
assert!(oracle_price_data.price == 60 * QUOTE_PRECISION as i64);
assert!(oracle_price_data.price == 240 * QUOTE_PRECISION as i64);
}

#[test]
Expand Down Expand Up @@ -735,8 +784,7 @@ mod tests {
let mut oracles = [AccountWithKey {
key: Pubkey::new_unique(),
account: Account {
// encoded from pyth Price, see liquidation tests
data: SOL_PYTH_PRICE.clone(),
data: get_account_bytes(&mut get_pyth_price(240, 9)).to_vec(),
owner: constants::ids::pyth_program::ID,
..Default::default()
},
Expand All @@ -759,6 +807,138 @@ mod tests {
}
}

#[test]
fn ffi_simulate_place_perp_order() {
// smoke test for ffi compatability, logic tested in `math::` module
let btc_perp_index = 1_u16;
let mut user = User::default();
user.spot_positions[1] = SpotPosition {
market_index: 1,
scaled_balance: (1_000 * SPOT_BALANCE_PRECISION) as u64,
balance_type: SpotBalanceType::Deposit,
..Default::default()
};
user.perp_positions[0] = PerpPosition {
market_index: btc_perp_index,
base_asset_amount: 100 * BASE_PRECISION_I64 as i64,
quote_asset_amount: -5_000 * QUOTE_PRECISION as i64,
..Default::default()
};
user.perp_positions[1] = PerpPosition {
market_index: 0,
base_asset_amount: 100 * BASE_PRECISION_I64 as i64,
quote_asset_amount: -5_000 * QUOTE_PRECISION as i64,
..Default::default()
};

// Create mock accounts
let mut perp_markets = vec![
AccountWithKey {
key: Pubkey::new_unique(),
account: Account {
owner: crate::constants::PROGRAM_ID,
data: [
PerpMarket::DISCRIMINATOR.as_slice(),
bytemuck::bytes_of(&PerpMarket {
market_index: btc_perp_index,
status: MarketStatus::Active,
amm: AMM {
order_step_size: 1_000,
order_tick_size: 1_000,
..Default::default()
},
..Default::default()
}),
]
.concat()
.to_vec(),
..Default::default()
},
},
AccountWithKey {
key: Pubkey::new_unique(),
account: Account {
owner: crate::constants::PROGRAM_ID,
data: [
PerpMarket::DISCRIMINATOR.as_slice(),
bytemuck::bytes_of(&PerpMarket {
market_index: 0,
status: MarketStatus::Active,
amm: AMM {
order_step_size: 1_000,
order_tick_size: 1_000,
..Default::default()
},
..Default::default()
}),
]
.concat()
.to_vec(),
..Default::default()
},
},
];
let mut spot_markets = vec![
AccountWithKey {
key: Pubkey::new_unique(),
account: Account {
owner: crate::constants::PROGRAM_ID,
data: [
SpotMarket::DISCRIMINATOR.as_slice(),
bytemuck::bytes_of(&sol_spot_market()),
]
.concat()
.to_vec(),
..Default::default()
},
},
AccountWithKey {
key: Pubkey::new_unique(),
account: Account {
owner: crate::constants::PROGRAM_ID,
data: [
SpotMarket::DISCRIMINATOR.as_slice(),
bytemuck::bytes_of(&usdc_spot_market()),
]
.concat()
.to_vec(),
..Default::default()
},
},
];

create_account_info!(
get_pyth_price(240, 9),
&sol_spot_market().oracle,
pyth_program::ID,
sol_oracle
);
create_account_info!(
get_pyth_price(1, 6),
&usdc_spot_market().oracle,
pyth_program::ID,
usdc_oracle
);

let mut oracles = [sol_oracle, usdc_oracle];
let mut accounts = AccountsList::new(&mut perp_markets, &mut spot_markets, &mut oracles);

let res = simulate_place_perp_order(
&user,
&mut accounts,
&State::default(),
&OrderParams {
market_index: 1,
market_type: MarketType::Perp,
direction: PositionDirection::Short,
base_asset_amount: 123 * BASE_PRECISION as u64,
order_type: OrderType::Market,
..Default::default()
},
);
assert!(res.is_ok_and(|truthy| truthy))
}

#[test]
fn ffi_calculate_auction_price() {
let price = calculate_auction_price(
Expand Down
5 changes: 4 additions & 1 deletion crates/src/math/leverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,10 @@ impl UserMargin for DriftClient {
.worst_case_base_asset_amount(oracle_price, market.contract_type)?;

let margin_info = self.calculate_margin_info(user)?;
let free_collateral = margin_info.get_free_collateral() - collateral_buffer as u128;
let free_collateral = margin_info
.get_free_collateral()
.checked_sub(collateral_buffer as u128)
.ok_or(SdkError::MathError("underflow".to_string()))?;

let margin_ratio = market
.get_margin_ratio(
Expand Down
Loading

0 comments on commit 5ce0969

Please sign in to comment.