Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ffi perp order simulation #76

Merged
merged 4 commits into from
Nov 21, 2024
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
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
Loading