Skip to content

Commit

Permalink
program: auto derisk lp positions in settle pnl (#766)
Browse files Browse the repository at this point in the history
* bigz/add-lp-order-risk-mitigations

* friendly perp position iter

* update parmams mutability

* add time_since_last_liquidity_change

* add new variable to margin context mode

* add attempt to burn shares in force cancel/settle pnl

* remove from force cancel orders

* sdk: fix tests not building

* change place orders

* rmeove unused mut

* address some comments

* add to settle_pnl

* use oracle orders

* tweak order params

* oracle order tests

* add typescript test, fix order param market_type

* remove logic from settle_expired_position

* CHANGELOG

---------

Co-authored-by: Chris Heaney <[email protected]>
  • Loading branch information
0xbigz and crispheaney committed Jan 11, 2024
1 parent cd82421 commit 606ab63
Show file tree
Hide file tree
Showing 18 changed files with 1,376 additions and 159 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- sdk: add option to cancel existing orders in market for place and take order
- sdk: add option to get signed settlePnl tx back from a market order

- program: auto derisk lp positions in settle pnl ([#766](https://github.com/drift-labs/protocol-v2/pull/766))
- program: increase full perp liquidation threshold ([#807](https://github.com/drift-labs/protocol-v2/pull/807))
- program: remove spot fee pool transfer ([#800](https://github.com/drift-labs/protocol-v2/pull/800))
- program: increase insurance tier max ([#784](https://github.com/drift-labs/protocol-v2/pull/784))
Expand Down
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
151 changes: 134 additions & 17 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 @@ -55,11 +56,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 @@ -91,7 +94,8 @@ mod amm_lp_jit_tests;

pub fn place_perp_order(
state: &State,
user: &AccountLoader<User>,
user: &mut User,
user_key: Pubkey,
perp_market_map: &PerpMarketMap,
spot_market_map: &SpotMarketMap,
oracle_map: &mut OracleMap,
Expand All @@ -101,8 +105,6 @@ pub fn place_perp_order(
) -> 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 @@ -748,15 +750,14 @@ pub fn modify_order(

user.update_last_active_slot(clock.slot);

drop(user);

let order_params =
merge_modify_order_params_with_existing_order(&existing_order, &modify_order_params)?;

if order_params.market_type == MarketType::Perp {
place_perp_order(
state,
user_loader,
&mut user,
user_key,
perp_market_map,
spot_market_map,
oracle_map,
Expand All @@ -767,7 +768,8 @@ pub fn modify_order(
} else {
place_spot_order(
state,
user_loader,
&mut user,
user_key,
perp_market_map,
spot_market_map,
oracle_map,
Expand Down Expand Up @@ -2706,7 +2708,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 @@ -2717,8 +2719,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 @@ -2728,8 +2730,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 @@ -2818,6 +2827,115 @@ 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,
market_index: u16,
) -> 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()? {
burn_user_lp_shares_for_risk_reduction(
state,
user,
user_key,
market_index,
perp_market_map,
spot_market_map,
oracle_map,
clock,
)?;
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,
) -> DriftResult {
let position_index = get_position_index(&user.perp_positions, market_index)?;
let is_lp = user.perp_positions[position_index].is_lp();
if !is_lp {
return Ok(());
}

let lp_shares = user.perp_positions[position_index].lp_shares;

let mut market = perp_market_map.get_ref_mut(&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 order_step_size = market.amm.order_step_size;
let (position_delta, pnl) = burn_lp_shares(
&mut user.perp_positions[position_index],
&mut market,
lp_shares.safe_div(3)?.max(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,
delta_base_asset_amount: position_delta.base_asset_amount,
delta_quote_asset_amount: position_delta.quote_asset_amount,
pnl,
})?;

let direction_to_close = user.perp_positions[position_index].get_direction_to_close();

let params = OrderParams::get_close_perp_params(
&market,
direction_to_close,
user.perp_positions[position_index]
.base_asset_amount
.unsigned_abs(),
)?;

drop(market);

controller::orders::place_perp_order(
state,
user,
user_key,
perp_market_map,
spot_market_map,
oracle_map,
clock,
params,
PlaceOrderOptions::default(),
)?;

Ok(())
}

pub fn pay_keeper_flat_reward_for_perps(
user: &mut User,
filler: Option<&mut User>,
Expand Down Expand Up @@ -2893,7 +3011,8 @@ pub fn pay_keeper_flat_reward_for_spot(

pub fn place_spot_order(
state: &State,
user: &AccountLoader<User>,
user: &mut User,
user_key: Pubkey,
perp_market_map: &PerpMarketMap,
spot_market_map: &SpotMarketMap,
oracle_map: &mut OracleMap,
Expand All @@ -2903,8 +3022,6 @@ pub fn place_spot_order(
) -> 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
Loading

0 comments on commit 606ab63

Please sign in to comment.