Skip to content

Commit

Permalink
Merge pull request #5 from drift-labs/crispheaney/arb-perp
Browse files Browse the repository at this point in the history
program: add arb_perp ix
  • Loading branch information
NourAlharithi authored Oct 16, 2023
2 parents 127b7ac + 354fd93 commit aa52e16
Show file tree
Hide file tree
Showing 11 changed files with 938 additions and 583 deletions.
280 changes: 139 additions & 141 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions programs/jit-proxy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ cpi = ["no-entrypoint"]
default = []

[dependencies]
anchor-lang = { git = "https://github.com/drift-labs/anchor.git", rev = "ed950fe", version = "0.26.0" }
anchor-spl = { git = "https://github.com/drift-labs/anchor.git", rev = "ed950fe", version = "0.26.0" }
drift = { git = "https://github.com/drift-labs/protocol-v2.git", rev = "099cbca", features = ["cpi", "mainnet-beta"]}
anchor-lang = "0.27.0"
anchor-spl = "0.27.0"
drift = { git = "https://github.com/drift-labs/protocol-v2.git", rev = "e5cdd4", features = ["cpi", "mainnet-beta"]}
bytemuck = { version = "1.4.0" }
static_assertions = "1.1.0"
22 changes: 22 additions & 0 deletions programs/jit-proxy/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use anchor_lang::prelude::*;

#[error_code]
#[derive(PartialEq, Eq)]
pub enum ErrorCode {
#[msg("BidNotCrossed")]
BidNotCrossed,
#[msg("AskNotCrossed")]
AskNotCrossed,
#[msg("TakerOrderNotFound")]
TakerOrderNotFound,
#[msg("OrderSizeBreached")]
OrderSizeBreached,
#[msg("NoBestBid")]
NoBestBid,
#[msg("NoBestAsk")]
NoBestAsk,
#[msg("NoArbOpportunity")]
NoArbOpportunity,
#[msg("UnprofitableArb")]
UnprofitableArb,
}
135 changes: 135 additions & 0 deletions programs/jit-proxy/src/instructions/arb_perp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use anchor_lang::prelude::*;
use drift::controller::position::PositionDirection;
use drift::cpi::accounts::PlaceAndTake;
use drift::instructions::optional_accounts::{load_maps, AccountMaps};
use drift::instructions::{OrderParams, PostOnlyParam};
use drift::program::Drift;
use drift::state::perp_market_map::MarketSet;

use drift::math::orders::find_bids_and_asks_from_users;
use drift::state::state::State;
use drift::state::user::{MarketType, OrderTriggerCondition, OrderType, User, UserStats};
use drift::state::user_map::load_user_maps;

use crate::error::ErrorCode;

pub fn arb_perp<'info>(
ctx: Context<'_, '_, '_, 'info, ArbPerp<'info>>,
market_index: u16,
) -> Result<()> {
let clock = Clock::get()?;
let slot = clock.slot;
let now = clock.unix_timestamp;

let taker = ctx.accounts.user.load()?;

let quote_init = taker
.get_perp_position(market_index)
.map_or(0, |p| p.quote_asset_amount);

let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable();
let AccountMaps {
perp_market_map,
mut oracle_map,
..
} = load_maps(
remaining_accounts_iter,
&MarketSet::new(),
&MarketSet::new(),
slot,
None,
)?;

let (makers, _) = load_user_maps(remaining_accounts_iter, true)?;

let perp_market = perp_market_map.get_ref(&market_index)?;
let oracle_price_data = oracle_map.get_price_data(&perp_market.amm.oracle)?;

let (bids, asks) =
find_bids_and_asks_from_users(&perp_market, oracle_price_data, &makers, slot, now)?;

let best_bid = bids.first().ok_or(ErrorCode::NoBestBid)?;
let best_ask = asks.first().ok_or(ErrorCode::NoBestAsk)?;

if best_bid.price < best_ask.price {
return Err(ErrorCode::NoArbOpportunity.into());
}

let base_asset_amount = best_bid.base_asset_amount.min(best_ask.base_asset_amount);

let get_order_params = |taker_direction: PositionDirection, taker_price: u64| -> OrderParams {
OrderParams {
order_type: OrderType::Limit,
market_type: MarketType::Perp,
direction: taker_direction,
user_order_id: 0,
base_asset_amount,
price: taker_price,
market_index,
reduce_only: false,
post_only: PostOnlyParam::None,
immediate_or_cancel: true,
max_ts: None,
trigger_price: None,
trigger_condition: OrderTriggerCondition::Above,
oracle_price_offset: None,
auction_duration: None,
auction_start_price: None,
auction_end_price: None,
}
};

let order_params = vec![
get_order_params(PositionDirection::Long, best_ask.price),
get_order_params(PositionDirection::Short, best_bid.price),
];

drop(taker);
drop(perp_market);

place_and_take(&ctx, order_params)?;

let taker = ctx.accounts.user.load()?;
let quote_end = taker
.get_perp_position(market_index)
.map_or(0, |p| p.quote_asset_amount);

if quote_end <= quote_init {
return Err(ErrorCode::NoArbOpportunity.into());
}

Ok(())
}

#[derive(Accounts)]
pub struct ArbPerp<'info> {
pub state: Box<Account<'info, State>>,
#[account(mut)]
pub user: AccountLoader<'info, User>,
#[account(mut)]
pub user_stats: AccountLoader<'info, UserStats>,
pub authority: Signer<'info>,
pub drift_program: Program<'info, Drift>,
}

fn place_and_take<'info>(
ctx: &Context<'_, '_, '_, 'info, ArbPerp<'info>>,
orders_params: Vec<OrderParams>,
) -> Result<()> {
for order_params in orders_params {
let drift_program = ctx.accounts.drift_program.to_account_info().clone();
let cpi_accounts = PlaceAndTake {
state: ctx.accounts.state.to_account_info().clone(),
user: ctx.accounts.user.to_account_info().clone(),
user_stats: ctx.accounts.user_stats.to_account_info().clone(),
authority: ctx.accounts.authority.to_account_info().clone(),
};

let cpi_context = CpiContext::new(drift_program, cpi_accounts)
.with_remaining_accounts(ctx.remaining_accounts.into());

drift::cpi::place_and_take_perp_order(cpi_context, order_params, None)?;
}

Ok(())
}
123 changes: 123 additions & 0 deletions programs/jit-proxy/src/instructions/check_order_constraints.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use anchor_lang::prelude::*;
use drift::instructions::optional_accounts::{load_maps, AccountMaps};
use drift::math::casting::Cast;
use drift::math::safe_math::SafeMath;
use drift::state::perp_market_map::MarketSet;
use drift::state::user::User;

use crate::error::ErrorCode;
use crate::state::MarketType;

pub fn check_order_constraints<'info>(
ctx: Context<'_, '_, '_, 'info, CheckOrderConstraints<'info>>,
constraints: Vec<OrderConstraint>,
) -> Result<()> {
let clock = Clock::get()?;
let slot = clock.slot;

let user = ctx.accounts.user.load()?;

let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable();
let AccountMaps {
perp_market_map,
spot_market_map,
mut oracle_map,
} = load_maps(
remaining_accounts_iter,
&MarketSet::new(),
&MarketSet::new(),
slot,
None,
)?;

for constraint in constraints.iter() {
if constraint.market_type == MarketType::Spot {
let spot_market = spot_market_map.get_ref(&constraint.market_index)?;
let spot_position = match user.get_spot_position(constraint.market_index) {
Ok(spot_position) => spot_position,
Err(_) => continue,
};

let signed_token_amount = spot_position
.get_signed_token_amount(&spot_market)?
.cast::<i64>()?;

constraint.check(
signed_token_amount,
spot_position.open_bids,
spot_position.open_asks,
)?;
} else {
let perp_market = perp_market_map.get_ref(&constraint.market_index)?;
let perp_position = match user.get_perp_position(constraint.market_index) {
Ok(perp_position) => perp_position,
Err(_) => continue,
};

let oracle_price = oracle_map.get_price_data(&perp_market.amm.oracle)?.price;

let settled_perp_position =
perp_position.simulate_settled_lp_position(&perp_market, oracle_price)?;

constraint.check(
settled_perp_position.base_asset_amount,
settled_perp_position.open_bids,
settled_perp_position.open_asks,
)?;
}
}

Ok(())
}

#[derive(Accounts)]
pub struct CheckOrderConstraints<'info> {
pub user: AccountLoader<'info, User>,
}

#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)]
pub struct OrderConstraint {
pub max_position: i64,
pub min_position: i64,
pub market_index: u16,
pub market_type: MarketType,
}

impl OrderConstraint {
pub fn check(&self, current_position: i64, open_bids: i64, open_asks: i64) -> Result<()> {
let max_long = current_position.safe_add(open_bids)?;

if max_long > self.max_position {
msg!(
"market index {} market type {:?}",
self.market_index,
self.market_type
);
msg!(
"max long {} current position {} open bids {}",
max_long,
current_position,
open_bids
);
return Err(ErrorCode::OrderSizeBreached.into());
}

let max_short = current_position.safe_add(open_asks)?;
if max_short < self.min_position {
msg!(
"market index {} market type {:?}",
self.market_index,
self.market_type
);
msg!(
"max short {} current position {} open asks {}",
max_short,
current_position,
open_asks
);
return Err(ErrorCode::OrderSizeBreached.into());
}

Ok(())
}
}
Loading

0 comments on commit aa52e16

Please sign in to comment.