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

bigz/add-lp-order-risk-mitigations #766

Merged
merged 21 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
41 changes: 26 additions & 15 deletions programs/drift/src/controller/lp/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ use crate::state::state::{OracleGuardRails, State, ValidityGuardRails};
use crate::state::user::{SpotPosition, User};
use crate::test_utils::*;
use crate::test_utils::{get_positions, get_pyth_price, get_spot_positions};
use anchor_lang::prelude::Clock;

#[test]
fn test_lp_wont_collect_improper_funding() {
let mut position = PerpPosition {
Expand Down Expand Up @@ -433,8 +435,14 @@ pub fn test_lp_settle_pnl() {
&pyth_program,
oracle_account_info
);
let slot = 0;
let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap();
let clock = Clock {
slot: 0,
epoch_start_timestamp: 0,
epoch: 0,
leader_schedule_epoch: 0,
unix_timestamp: 0,
};
let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock.slot, None).unwrap();

let mut market = PerpMarket {
amm: AMM {
Expand Down Expand Up @@ -516,8 +524,6 @@ pub fn test_lp_settle_pnl() {
..User::default()
};

let now = 1000000;

let state = State {
oracle_guard_rails: OracleGuardRails {
validity: ValidityGuardRails {
Expand Down Expand Up @@ -555,7 +561,7 @@ pub fn test_lp_settle_pnl() {
&market_map,
&spot_market_map,
&mut oracle_map,
now,
&clock,
&state,
);

Expand Down Expand Up @@ -728,16 +734,21 @@ fn test_lp_margin_calc() {

let strict_quote_price = StrictOraclePrice::test(1000000);
// ensure margin calc doesnt incorrectly count funding rate (funding pnl MUST come before settling lp)
let (margin_requirement, weighted_unrealized_pnl, worse_case_base_asset_value) =
calculate_perp_position_value_and_pnl(
&user.perp_positions[0],
&market,
&oracle_price_data,
&strict_quote_price,
crate::math::margin::MarginRequirementType::Initial,
0,
)
.unwrap();
let (
margin_requirement,
weighted_unrealized_pnl,
worse_case_base_asset_value,
_open_order_fraction,
) = calculate_perp_position_value_and_pnl(
&user.perp_positions[0],
&market,
&oracle_price_data,
&strict_quote_price,
crate::math::margin::MarginRequirementType::Initial,
0,
false,
)
.unwrap();

assert_eq!(margin_requirement, 1012000000); // $1010 + $2 mr for lp_shares
assert_eq!(weighted_unrealized_pnl, -9916900000); // $-9900000000 upnl (+ -16900000 from old funding)
Expand Down
177 changes: 167 additions & 10 deletions programs/drift/src/controller/orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use solana_program::msg;

use crate::controller;
use crate::controller::funding::settle_funding_payment;
use crate::controller::lp::burn_lp_shares;
use crate::controller::position;
use crate::controller::position::{
add_new_position, decrease_open_bids_and_asks, get_position_index, increase_open_bids_and_asks,
Expand Down Expand Up @@ -57,11 +58,13 @@ use crate::math::amm::calculate_amm_available_liquidity;
use crate::math::safe_unwrap::SafeUnwrap;
use crate::math::spot_swap::select_margin_type_for_swap;
use crate::print_error;
use crate::state::events::{emit_stack, get_order_action_record, OrderActionRecord, OrderRecord};
use crate::state::events::{
emit_stack, get_order_action_record, LPAction, LPRecord, OrderActionRecord, OrderRecord,
};
use crate::state::events::{OrderAction, OrderActionExplanation};
use crate::state::fill_mode::FillMode;
use crate::state::fulfillment::{PerpFulfillmentMethod, SpotFulfillmentMethod};
use crate::state::margin_calculation::MarginContext;
use crate::state::margin_calculation::{MarginCalculation, MarginContext};
use crate::state::oracle::{OraclePriceData, StrictOraclePrice};
use crate::state::oracle_map::OracleMap;
use crate::state::perp_market::{AMMLiquiditySplit, MarketStatus, PerpMarket};
Expand Down Expand Up @@ -99,12 +102,36 @@ pub fn place_perp_order(
oracle_map: &mut OracleMap,
clock: &Clock,
mut params: OrderParams,
options: PlaceOrderOptions,
) -> DriftResult {
let user_key = user.key();
let user = &mut load_mut!(user)?;
_place_perp_order(
state,
user,
user_key,
perp_market_map,
spot_market_map,
oracle_map,
clock,
&mut params,
options,
)
}

pub fn _place_perp_order(
state: &State,
user: &mut User,
user_key: Pubkey,
perp_market_map: &PerpMarketMap,
spot_market_map: &SpotMarketMap,
oracle_map: &mut OracleMap,
clock: &Clock,
params: &mut OrderParams,
mut options: PlaceOrderOptions,
) -> DriftResult {
let now = clock.unix_timestamp;
let slot = clock.slot;
let user_key = user.key();
let user = &mut load_mut!(user)?;

validate_user_not_being_liquidated(
user,
Expand Down Expand Up @@ -225,7 +252,7 @@ pub fn place_perp_order(
params.update_perp_auction_params(market, oracle_price_data.price)?;

let (auction_start_price, auction_end_price, auction_duration) = get_auction_params(
&params,
params,
oracle_price_data,
market.amm.order_tick_size,
state.min_perp_auction_duration,
Expand Down Expand Up @@ -2708,7 +2735,7 @@ fn update_trigger_order_params(

pub fn force_cancel_orders(
state: &State,
user: &AccountLoader<User>,
user_account_loader: &AccountLoader<User>,
spot_market_map: &SpotMarketMap,
perp_market_map: &PerpMarketMap,
oracle_map: &mut OracleMap,
Expand All @@ -2719,8 +2746,8 @@ pub fn force_cancel_orders(
let slot = clock.slot;

let filler_key = filler.key();
let user_key = user.key();
let user = &mut load_mut!(user)?;
let user_key = user_account_loader.key();
let user = &mut load_mut!(user_account_loader)?;
let filler = &mut load_mut!(filler)?;

validate!(
Expand All @@ -2730,8 +2757,15 @@ pub fn force_cancel_orders(

validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?;

let meets_initial_margin_requirement =
meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?;
let margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info(
user,
perp_market_map,
spot_market_map,
oracle_map,
MarginContext::standard(MarginRequirementType::Initial),
)?;

let meets_initial_margin_requirement = margin_calc.meets_margin_requirement();

validate!(
!meets_initial_margin_requirement,
Expand Down Expand Up @@ -2800,6 +2834,17 @@ pub fn force_cancel_orders(
)?;
}

attempt_burn_user_lp_shares_for_risk_reduction(
state,
user,
margin_calc,
&user_key,
perp_market_map,
spot_market_map,
oracle_map,
clock,
)?;

pay_keeper_flat_reward_for_spot(
user,
Some(filler),
Expand All @@ -2820,6 +2865,118 @@ pub fn can_reward_user_with_perp_pnl(user: &mut Option<&mut User>, market_index:
}
}

pub fn attempt_burn_user_lp_shares_for_risk_reduction(
state: &State,
user: &mut User,
margin_calc: MarginCalculation,
user_key: &Pubkey,
perp_market_map: &PerpMarketMap,
spot_market_map: &SpotMarketMap,
oracle_map: &mut OracleMap,
clock: &Clock,
) -> DriftResult {
let now = clock.unix_timestamp;
// attempt to burn lp shares if user has a custom margin ratio set and its breached with orders
if !margin_calc.positions_meets_margin_requirement()? {
let time_since_last_liquidity_change: i64 =
now.safe_sub(user.last_add_perp_lp_shares_ts)?;
// avoid spamming update if orders have already been set
if time_since_last_liquidity_change >= state.lp_cooldown_time.cast()? {
let set_reduce_only_orders = true; // todo
for position_index in 0..user.perp_positions.len() {
let market_index = user.perp_positions[position_index].market_index;
_burn_user_lp_shares_for_risk_reduction(
state,
user,
user_key,
market_index,
perp_market_map,
spot_market_map,
oracle_map,
clock,
set_reduce_only_orders,
)?;
}
user.last_add_perp_lp_shares_ts = now;
}
}

Ok(())
}

pub fn _burn_user_lp_shares_for_risk_reduction(
state: &State,
user: &mut User,
user_key: &Pubkey,
market_index: u16,
perp_market_map: &PerpMarketMap,
spot_market_map: &SpotMarketMap,
oracle_map: &mut OracleMap,
clock: &Clock,
set_reduce_only_orders: bool,
) -> DriftResult {
let perp_position = user.get_perp_position_mut(market_index)?;
if perp_position.is_lp() {
let lp_shares = perp_position.lp_shares;
let market_index = perp_position.market_index;
{
let market = perp_market_map.get_ref(&market_index)?;
let oracle_price_data = oracle_map.get_price_data(&market.amm.oracle)?;

let oracle_price = if market.status == MarketStatus::Settlement {
market.expiry_price
} else {
oracle_price_data.price
};

let (position_delta, pnl) = burn_lp_shares(
perp_position,
perp_market_map.get_ref_mut(&market_index)?.deref_mut(),
lp_shares.safe_div(3)?.max(market.amm.order_step_size),
oracle_price,
)?;

// emit LP record for shares removed
emit_stack::<_, { LPRecord::SIZE }>(LPRecord {
ts: clock.unix_timestamp,
action: LPAction::RemoveLiquidity,
user: *user_key,
n_shares: lp_shares,
market_index: perp_position.market_index,
delta_base_asset_amount: position_delta.base_asset_amount,
delta_quote_asset_amount: position_delta.quote_asset_amount,
pnl,
})?;
}

if set_reduce_only_orders {
let market = perp_market_map.get_ref(&market_index)?;

let direction_to_close = perp_position.get_direction_to_close();

let mut params = OrderParams::get_aggressive_close_params(
&market,
direction_to_close,
perp_position.base_asset_amount.unsigned_abs(),
)?;

controller::orders::_place_perp_order(
state,
user,
*user_key,
perp_market_map,
spot_market_map,
oracle_map,
clock,
&mut params,
PlaceOrderOptions::default(),
)?;
}
}

Ok(())
}

pub fn pay_keeper_flat_reward_for_perps(
user: &mut User,
filler: Option<&mut User>,
Expand Down
Loading