diff --git a/CHANGELOG.md b/CHANGELOG.md index eb573c0cac..acf18ce56f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- program: perp position max margin ratio ([#1847](https://github.com/drift-labs/protocol-v2/pull/1847)) - program: add padding to swift messages ([#1845](https://github.com/drift-labs/protocol-v2/pull/1845)) - program: rm lp ([#1755](https://github.com/drift-labs/protocol-v2/pull/1755)) diff --git a/programs/drift/src/controller/position.rs b/programs/drift/src/controller/position.rs index 97add169ea..6b6ec530d5 100644 --- a/programs/drift/src/controller/position.rs +++ b/programs/drift/src/controller/position.rs @@ -49,8 +49,19 @@ pub fn add_new_position( .position(|market_position| market_position.is_available()) .ok_or(ErrorCode::MaxNumberOfPositions)?; + let max_margin_ratio = { + let old_position = &user_positions[new_position_index]; + + if old_position.market_index == market_index { + old_position.max_margin_ratio + } else { + 0_u16 + } + }; + let new_market_position = PerpPosition { market_index, + max_margin_ratio, ..PerpPosition::default() }; diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 1d29f0d46c..2bf1a003a1 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -1,4 +1,5 @@ use std::cell::RefMut; +use std::collections::BTreeMap; use std::convert::TryFrom; use anchor_lang::prelude::*; @@ -26,6 +27,7 @@ use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; use crate::math::casting::Cast; use crate::math::constants::QUOTE_SPOT_MARKET_INDEX; +use crate::math::margin::get_margin_calculation_for_disable_high_leverage_mode; use crate::math::margin::{calculate_user_equity, meets_settle_pnl_maintenance_margin_requirement}; use crate::math::orders::{estimate_price_from_side, find_bids_and_asks_from_users}; use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; @@ -2803,20 +2805,13 @@ pub fn handle_disable_user_high_leverage_mode<'c: 'info, 'info>( } } - let custom_margin_ratio_before = user.max_margin_ratio; - user.max_margin_ratio = 0; - - let margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info( - &user, + let margin_calc = get_margin_calculation_for_disable_high_leverage_mode( + &mut user, &perp_market_map, &spot_market_map, &mut oracle_map, - MarginContext::standard(MarginRequirementType::Initial) - .margin_buffer(MARGIN_PRECISION / 100), // 1% buffer )?; - user.max_margin_ratio = custom_margin_ratio_before; - if margin_calc.num_perp_liabilities > 0 { let mut requires_invariant_check = false; diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 86229bc1bd..7528413700 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -2945,6 +2945,20 @@ pub fn handle_update_user_custom_margin_ratio( Ok(()) } +pub fn handle_update_user_perp_position_custom_margin_ratio( + ctx: Context, + _sub_account_id: u16, + perp_market_index: u16, + margin_ratio: u16, +) -> Result<()> { + let mut user = load_mut!(ctx.accounts.user)?; + + user.update_perp_position_max_margin_ratio(perp_market_index, margin_ratio)?; + + Ok(()) +} + + pub fn handle_update_user_margin_trading_enabled<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, UpdateUser<'info>>, _sub_account_id: u16, @@ -4432,6 +4446,21 @@ pub struct UpdateUser<'info> { pub authority: Signer<'info>, } +#[derive(Accounts)] +#[instruction( + sub_account_id: u16, +)] +pub struct UpdateUserPerpPositionCustomMarginRatio<'info> { + #[account( + mut, + seeds = [b"user", authority.key.as_ref(), sub_account_id.to_le_bytes().as_ref()], + bump, + constraint = can_sign_for_user(&user, &authority)? + )] + pub user: AccountLoader<'info, User>, + pub authority: Signer<'info>, +} + #[derive(Accounts)] pub struct DeleteUser<'info> { #[account( diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 9a14a80e59..964a7851ca 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -367,6 +367,15 @@ pub mod drift { handle_update_user_custom_margin_ratio(ctx, _sub_account_id, margin_ratio) } + pub fn update_user_perp_position_custom_margin_ratio( + ctx: Context, + _sub_account_id: u16, + perp_market_index: u16, + margin_ratio: u16, + ) -> Result<()> { + handle_update_user_perp_position_custom_margin_ratio(ctx, _sub_account_id, perp_market_index, margin_ratio) + } + pub fn update_user_margin_trading_enabled<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, UpdateUser<'info>>, _sub_account_id: u16, diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index f62bbb6122..38bbe29697 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -6,6 +6,7 @@ use crate::math::constants::{ }; use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; +use crate::MARGIN_PRECISION; use crate::{validate, PRICE_PRECISION_I128}; use crate::{validation, PRICE_PRECISION_I64}; @@ -27,6 +28,7 @@ use crate::state::spot_market_map::SpotMarketMap; use crate::state::user::{MarketType, OrderFillSimulation, PerpPosition, User}; use num_integer::Roots; use std::cmp::{max, min, Ordering}; +use std::collections::BTreeMap; #[cfg(test)] mod tests; @@ -535,6 +537,12 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( 0, )?; + let perp_position_custom_margin_ratio = if context.margin_type == MarginRequirementType::Initial { + market_position.max_margin_ratio as u32 + } else { + 0_u32 + }; + let ( perp_margin_requirement, weighted_pnl, @@ -547,7 +555,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( oracle_price_data, &strict_quote_price, context.margin_type, - user_custom_margin_ratio, + user_custom_margin_ratio.max(perp_position_custom_margin_ratio), user_high_leverage_mode, calculation.track_open_orders_fraction(), )?; @@ -884,6 +892,42 @@ pub fn validate_spot_margin_trading( Ok(()) } +pub fn get_margin_calculation_for_disable_high_leverage_mode( + user: &mut User, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, +) -> DriftResult { + let custom_margin_ratio_before = user.max_margin_ratio; + + + let mut perp_position_max_margin_ratio_map = BTreeMap::new(); + for (index, position) in user.perp_positions.iter_mut().enumerate() { + if position.max_margin_ratio == 0 { + continue; + } + + perp_position_max_margin_ratio_map.insert(index, position.max_margin_ratio); + position.max_margin_ratio = 0; + } + + let margin_buffer = MARGIN_PRECISION / 100; // 1% buffer + 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).margin_buffer(margin_buffer), + )?; + + user.max_margin_ratio = custom_margin_ratio_before; + for (index, perp_position_max_margin_ratio) in perp_position_max_margin_ratio_map.iter() { + user.perp_positions[*index].max_margin_ratio = *perp_position_max_margin_ratio; + } + + Ok(margin_calc) +} + pub fn calculate_user_equity( user: &User, perp_market_map: &PerpMarketMap, diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 7a256b65db..edfcf49984 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -1082,6 +1082,132 @@ mod calculate_margin_requirement_and_total_collateral { assert_eq!(total_collateral, 5000000000); // 100 * $100 * .5 } + #[test] + pub fn user_perp_positions_custom_margin_ratio() { + let slot = 0_u64; + + let mut sol_oracle_price = get_pyth_price(100, 6); + let sol_oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + sol_oracle_price, + &sol_oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + order_step_size: 10000000, + oracle: sol_oracle_price_key, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + status: MarketStatus::Initialized, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let perp_market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + historical_oracle_data: HistoricalOracleData::default_quote_oracle(), + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let mut sol_spot_market = SpotMarket { + market_index: 1, + oracle_source: OracleSource::Pyth, + oracle: sol_oracle_price_key, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + initial_asset_weight: 8 * SPOT_WEIGHT_PRECISION / 10, + maintenance_asset_weight: 9 * SPOT_WEIGHT_PRECISION / 10, + initial_liability_weight: 12 * SPOT_WEIGHT_PRECISION / 10, + maintenance_liability_weight: 11 * SPOT_WEIGHT_PRECISION / 10, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 1000, + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_spot_market, SpotMarket, sol_spot_market_account_info); + let spot_market_account_infos = Vec::from([ + &usdc_spot_market_account_info, + &sol_spot_market_account_info, + ]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + + let user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: 100 * BASE_PRECISION_I64, + max_margin_ratio: 2 * MARGIN_PRECISION as u16, // .5x leverage + ..PerpPosition::default() + }), + spot_positions, + ..User::default() + }; + + let MarginCalculation { + margin_requirement, .. + } = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial), + ) + .unwrap(); + + assert_eq!(margin_requirement, 20000000000); + + let user = User { + max_margin_ratio: 4 * MARGIN_PRECISION, // 1x leverage + ..user + }; + + let MarginCalculation { + margin_requirement, .. + } = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial), + ) + .unwrap(); + + // user custom margin ratio should override perp position custom margin ratio + assert_eq!(margin_requirement, 40000000000); + } + #[test] pub fn user_dust_deposit() { let slot = 0_u64; @@ -4318,3 +4444,150 @@ mod pools { assert_eq!(result.unwrap_err(), ErrorCode::InvalidPoolId) } } + +#[cfg(test)] +mod get_margin_calculation_for_disable_high_leverage_mode { + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::{create_account_info, MARGIN_PRECISION}; + use crate::math::constants::{ + AMM_RESERVE_PRECISION, LIQUIDATION_FEE_PRECISION, PEG_PRECISION, + SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, + SPOT_WEIGHT_PRECISION, + }; + use crate::math::margin::get_margin_calculation_for_disable_high_leverage_mode; + use crate::state::oracle::{HistoricalOracleData, OracleSource}; + use crate::state::oracle_map::OracleMap; + use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; + use crate::state::perp_market_map::PerpMarketMap; + use crate::state::spot_market::{SpotBalanceType, SpotMarket}; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::user::{Order, PerpPosition, SpotPosition, User}; + use crate::test_utils::*; + use crate::test_utils::get_pyth_price; + use crate::create_anchor_account_info; + + #[test] + pub fn check_user_not_changed() { + let slot = 0_u64; + + let mut sol_oracle_price = get_pyth_price(100, 6); + let sol_oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + sol_oracle_price, + &sol_oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + order_step_size: 10000000, + oracle: sol_oracle_price_key, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + status: MarketStatus::Initialized, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let perp_market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + historical_oracle_data: HistoricalOracleData::default_quote_oracle(), + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let mut sol_spot_market = SpotMarket { + market_index: 1, + oracle_source: OracleSource::Pyth, + oracle: sol_oracle_price_key, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + initial_asset_weight: 8 * SPOT_WEIGHT_PRECISION / 10, + maintenance_asset_weight: 9 * SPOT_WEIGHT_PRECISION / 10, + initial_liability_weight: 12 * SPOT_WEIGHT_PRECISION / 10, + maintenance_liability_weight: 11 * SPOT_WEIGHT_PRECISION / 10, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 1000, + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_spot_market, SpotMarket, sol_spot_market_account_info); + let spot_market_account_infos = Vec::from([ + &usdc_spot_market_account_info, + &sol_spot_market_account_info, + ]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 20000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + spot_positions[1] = SpotPosition { + market_index: 1, + balance_type: SpotBalanceType::Borrow, + scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + + let mut perp_positions = [PerpPosition::default(); 8]; + perp_positions[1] = PerpPosition { + market_index: 0, + max_margin_ratio: 2 * MARGIN_PRECISION as u16, // .5x leverage + ..PerpPosition::default() + }; + perp_positions[7] = PerpPosition { + market_index: 1, + max_margin_ratio: 5 * MARGIN_PRECISION as u16, // .5x leverage + ..PerpPosition::default() + }; + + let mut user = User { + orders: [Order::default(); 32], + perp_positions, + spot_positions, + max_margin_ratio: 2 * MARGIN_PRECISION as u32, // .5x leverage + ..User::default() + }; + + let user_before = user.clone(); + + get_margin_calculation_for_disable_high_leverage_mode( + &mut user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + + // should not change user + assert_eq!(user, user_before); + } +} \ No newline at end of file diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 87dd21bab4..2a74eab772 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -618,6 +618,19 @@ impl User { return Ok(true); } + + pub fn update_perp_position_max_margin_ratio(&mut self, market_index: u16, margin_ratio: u16) -> DriftResult<()> { + if self.max_margin_ratio > margin_ratio as u32 { + msg!("user.max_margin_ratio ({}) > margin_ratio ({}), setting user.max_margin_ratio to margin_ratio", self.max_margin_ratio, margin_ratio); + self.max_margin_ratio = margin_ratio as u32; + } + + let perp_position = self.force_get_perp_position_mut(market_index)?; + msg!("perp_position.max_margin_ratio ({}) -> {}", perp_position.max_margin_ratio, margin_ratio); + perp_position.max_margin_ratio = margin_ratio; + + Ok(()) + } } pub fn derive_user_account(authority: &Pubkey, sub_account_id: u16) -> Pubkey { @@ -958,10 +971,9 @@ pub struct PerpPosition { /// Used to settle the users lp position /// precision: QUOTE_PRECISION pub last_quote_asset_amount_per_lp: i64, - /// Settling LP position can lead to a small amount of base asset being left over smaller than step size - /// This records that remainder so it can be settled later on - /// precision: BASE_PRECISION - pub remainder_base_asset_amount: i32, + pub padding: [u8; 2], + // custom max margin ratio for perp market + pub max_margin_ratio: u16, /// The market index for the perp market pub market_index: u16, /// The number of open orders diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 4e8d4f3be7..c1de9cca59 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2492,3 +2492,34 @@ mod update_open_bids_and_asks { assert!(order.update_open_bids_and_asks()); } } + +mod force_get_user_perp_position_mut { + use crate::state::user::{PerpPosition, PositionFlag, User}; + + #[test] + fn test() { + let mut user = User::default(); + + let perp_position = PerpPosition { + market_index: 0, + max_margin_ratio: 1, + ..PerpPosition::default() + }; + user.perp_positions[0] = perp_position; + + // if next available slot is same market index and has max margin ratio, persist it + { + let perp_position_mut = user.force_get_perp_position_mut(0).unwrap(); + assert_eq!(perp_position_mut.max_margin_ratio, 1); + } + + // if next available slot is has max margin but different market index, dont persist it + { + let perp_position_mut = user.force_get_perp_position_mut(2).unwrap(); + assert_eq!(perp_position_mut.max_margin_ratio, 0); + } + + assert_eq!(user.perp_positions[0].market_index, 2); + assert_eq!(user.perp_positions[0].max_margin_ratio, 0); + } +}