diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index 7162e9d2dd..cbfc47ff32 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -590,7 +590,7 @@ fn calculate_revenue_pool_transfer( pub fn update_pool_balances( market: &mut PerpMarket, spot_market: &mut SpotMarket, - user_quote_position: &SpotPosition, + user_quote_token_amount: i128, user_unsettled_pnl: i128, now: i64, ) -> DriftResult { @@ -738,12 +738,13 @@ pub fn update_pool_balances( let pnl_to_settle_with_user = if user_unsettled_pnl > 0 { min(user_unsettled_pnl, pnl_pool_token_amount.cast::()?) } else { - let token_amount = user_quote_position.get_signed_token_amount(spot_market)?; - // dont settle negative pnl to spot borrows when utilization is high (> 80%) - let max_withdraw_amount = - -get_max_withdraw_for_market_with_token_amount(spot_market, token_amount, false)? - .cast::()?; + let max_withdraw_amount = -get_max_withdraw_for_market_with_token_amount( + spot_market, + user_quote_token_amount, + false, + )? + .cast::()?; max_withdraw_amount.max(user_unsettled_pnl) }; @@ -783,7 +784,7 @@ pub fn update_pool_balances( pub fn update_pnl_pool_and_user_balance( market: &mut PerpMarket, - bank: &mut SpotMarket, + quote_spot_market: &mut SpotMarket, user: &mut User, unrealized_pnl_with_fee: i128, ) -> DriftResult { @@ -791,7 +792,7 @@ pub fn update_pnl_pool_and_user_balance( unrealized_pnl_with_fee.min( get_token_amount( market.pnl_pool.scaled_balance, - bank, + quote_spot_market, market.pnl_pool.balance_type(), )? .cast()?, @@ -822,14 +823,37 @@ pub fn update_pnl_pool_and_user_balance( return Ok(0); } - let user_spot_position = user.get_quote_spot_position_mut(); + let is_isolated_position = user.get_perp_position(market.market_index)?.is_isolated(); + if is_isolated_position { + let perp_position = user.force_get_isolated_perp_position_mut(market.market_index)?; + let perp_position_token_amount = + perp_position.get_isolated_token_amount(quote_spot_market)?; + + if pnl_to_settle_with_user < 0 { + validate!( + perp_position_token_amount >= pnl_to_settle_with_user.unsigned_abs(), + ErrorCode::InsufficientCollateral, + "user has insufficient deposit for market {}", + market.market_index + )?; + } - transfer_spot_balances( - pnl_to_settle_with_user, - bank, - &mut market.pnl_pool, - user_spot_position, - )?; + transfer_spot_balances( + pnl_to_settle_with_user, + quote_spot_market, + &mut market.pnl_pool, + perp_position, + )?; + } else { + let user_spot_position = user.get_quote_spot_position_mut(); + + transfer_spot_balances( + pnl_to_settle_with_user, + quote_spot_market, + &mut market.pnl_pool, + user_spot_position, + )?; + } Ok(pnl_to_settle_with_user) } diff --git a/programs/drift/src/controller/amm/tests.rs b/programs/drift/src/controller/amm/tests.rs index ba51779f5e..5147d85ef2 100644 --- a/programs/drift/src/controller/amm/tests.rs +++ b/programs/drift/src/controller/amm/tests.rs @@ -289,10 +289,11 @@ fn update_pool_balances_test_high_util_borrow() { let mut spot_position = SpotPosition::default(); let unsettled_pnl = -100; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -302,10 +303,11 @@ fn update_pool_balances_test_high_util_borrow() { // util is low => neg settle ok spot_market.borrow_balance = 0; let unsettled_pnl = -100; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -323,10 +325,12 @@ fn update_pool_balances_test_high_util_borrow() { false, ) .unwrap(); + + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -342,10 +346,12 @@ fn update_pool_balances_test_high_util_borrow() { false, ) .unwrap(); + + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -380,12 +386,26 @@ fn update_pool_balances_test() { let spot_position = SpotPosition::default(); - let to_settle_with_user = - update_pool_balances(&mut market, &mut spot_market, &spot_position, 100, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + let to_settle_with_user = update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 100, + now, + ) + .unwrap(); assert_eq!(to_settle_with_user, 0); - let to_settle_with_user = - update_pool_balances(&mut market, &mut spot_market, &spot_position, -100, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + let to_settle_with_user = update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + -100, + now, + ) + .unwrap(); assert_eq!(to_settle_with_user, -100); assert!(market.amm.fee_pool.balance() > 0); @@ -404,8 +424,15 @@ fn update_pool_balances_test() { assert_eq!(pnl_pool_token_amount, 99); assert_eq!(amm_fee_pool_token_amount, 1); - let to_settle_with_user = - update_pool_balances(&mut market, &mut spot_market, &spot_position, 100, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + let to_settle_with_user = update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 100, + now, + ) + .unwrap(); assert_eq!(to_settle_with_user, 99); let amm_fee_pool_token_amount = get_token_amount( market.amm.fee_pool.balance(), @@ -423,7 +450,15 @@ fn update_pool_balances_test() { assert_eq!(amm_fee_pool_token_amount, 1); market.amm.total_fee_minus_distributions = 0; - update_pool_balances(&mut market, &mut spot_market, &spot_position, -1, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + -1, + now, + ) + .unwrap(); let amm_fee_pool_token_amount = get_token_amount( market.amm.fee_pool.balance(), &spot_market, @@ -440,10 +475,11 @@ fn update_pool_balances_test() { assert_eq!(amm_fee_pool_token_amount, 0); market.amm.total_fee_minus_distributions = 90_000 * QUOTE_PRECISION as i128; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, -(100_000 * QUOTE_PRECISION as i128), now, ) @@ -466,10 +502,11 @@ fn update_pool_balances_test() { // negative fee pool market.amm.total_fee_minus_distributions = -8_008_123_456; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, 1_000_987_789, now, ) @@ -564,7 +601,15 @@ fn update_pool_balances_fee_to_revenue_test() { ); let spot_position = SpotPosition::default(); - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 50000000000000000); // under FEE_POOL_TO_REVENUE_POOL_THRESHOLD assert_eq!(market.pnl_pool.scaled_balance, 50000000000000000); @@ -579,7 +624,15 @@ fn update_pool_balances_fee_to_revenue_test() { let prev_fee_pool_2 = (FEE_POOL_TO_REVENUE_POOL_THRESHOLD + 50 * QUOTE_PRECISION) * SPOT_BALANCE_PRECISION; market.amm.fee_pool.scaled_balance = prev_fee_pool_2; - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.pnl_pool.scaled_balance, 50000000000000000); assert_eq!(market.amm.total_fee_withdrawn, 5000000); @@ -591,12 +644,28 @@ fn update_pool_balances_fee_to_revenue_test() { assert!(spot_market.revenue_pool.scaled_balance > prev_rev_pool); market.insurance_claim.quote_max_insurance = 1; // add min insurance - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.total_fee_withdrawn, 5000001); assert_eq!(spot_market.revenue_pool.scaled_balance, 5000001000000000); market.insurance_claim.quote_max_insurance = 100000000; // add lots of insurance - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.total_fee_withdrawn, 6000000); assert_eq!(spot_market.revenue_pool.scaled_balance, 6000000000000000); } @@ -675,7 +744,15 @@ fn update_pool_balances_fee_to_revenue_low_amm_revenue_test() { ); let spot_position = SpotPosition::default(); - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 50000000000000000); // under FEE_POOL_TO_REVENUE_POOL_THRESHOLD assert_eq!(market.pnl_pool.scaled_balance, 50000000000000000); @@ -690,7 +767,15 @@ fn update_pool_balances_fee_to_revenue_low_amm_revenue_test() { let prev_fee_pool_2 = (FEE_POOL_TO_REVENUE_POOL_THRESHOLD + 50 * QUOTE_PRECISION) * SPOT_BALANCE_PRECISION; market.amm.fee_pool.scaled_balance = prev_fee_pool_2; - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.pnl_pool.scaled_balance, 50000000000000000); assert_eq!(market.amm.total_fee_withdrawn, 1000000); @@ -704,14 +789,30 @@ fn update_pool_balances_fee_to_revenue_low_amm_revenue_test() { market.insurance_claim.quote_max_insurance = 1; // add min insurance market.amm.net_revenue_since_last_funding = 1; - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.total_fee_withdrawn, 1000001); assert_eq!(spot_market.revenue_pool.scaled_balance, 1000001000000000); market.insurance_claim.quote_max_insurance = 100000000; // add lots of insurance market.amm.net_revenue_since_last_funding = 100000000; - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.total_fee_withdrawn, 6000000); assert_eq!(spot_market.revenue_pool.scaled_balance, 6000000000000000); } @@ -807,7 +908,15 @@ fn update_pool_balances_revenue_to_fee_test() { 100 * SPOT_BALANCE_PRECISION ); - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!( market.amm.fee_pool.scaled_balance, @@ -838,7 +947,15 @@ fn update_pool_balances_revenue_to_fee_test() { ); assert_eq!(market.amm.total_fee_minus_distributions, -10000000000); - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!( market.amm.fee_pool.scaled_balance, @@ -863,7 +980,15 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(spot_market_vault_amount, 200000000); // total spot_market deposit balance unchanged during transfers // calling multiple times doesnt effect other than fee pool -> pnl pool - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!( market.amm.fee_pool.scaled_balance, 5 * SPOT_BALANCE_PRECISION @@ -873,7 +998,15 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(market.amm.total_fee_withdrawn, 0); assert_eq!(spot_market.revenue_pool.scaled_balance, 0); - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!( market.amm.fee_pool.scaled_balance, 5 * SPOT_BALANCE_PRECISION @@ -889,7 +1022,15 @@ fn update_pool_balances_revenue_to_fee_test() { let spot_market_backup = spot_market; let market_backup = market; - assert!(update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).is_err()); // assert is_err if any way has revenue pool above deposit balances + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + assert!(update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now + ) + .is_err()); // assert is_err if any way has revenue pool above deposit balances spot_market = spot_market_backup; market = market_backup; spot_market.deposit_balance += 9900000001000; @@ -902,7 +1043,15 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(spot_market.deposit_balance, 10100000001000); assert_eq!(spot_market_vault_amount, 10100000001); - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(spot_market.deposit_balance, 10100000001000); assert_eq!(spot_market.revenue_pool.scaled_balance, 9800000001000); assert_eq!(market.amm.fee_pool.scaled_balance, 105000000000); @@ -916,7 +1065,15 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(market.insurance_claim.last_revenue_withdraw_ts, now); // calling again only does fee -> pnl pool - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 5000000000); assert_eq!(market.pnl_pool.scaled_balance, 295000000000); assert_eq!(market.amm.total_fee_minus_distributions, -9800000000); @@ -929,7 +1086,15 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(market.insurance_claim.last_revenue_withdraw_ts, now); // calling again does nothing - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 5000000000); assert_eq!(market.pnl_pool.scaled_balance, 295000000000); assert_eq!(market.amm.total_fee_minus_distributions, -9800000000); @@ -978,9 +1143,15 @@ fn update_pool_balances_revenue_to_fee_test() { spot_market.revenue_pool.scaled_balance = 9800000001000; let market_backup = market; let spot_market_backup = spot_market; - assert!( - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now + 3600).is_err() - ); // assert is_err if any way has revenue pool above deposit balances + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + assert!(update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now + 3600 + ) + .is_err()); // assert is_err if any way has revenue pool above deposit balances market = market_backup; spot_market = spot_market_backup; spot_market.deposit_balance += 9800000000001; @@ -996,8 +1167,23 @@ fn update_pool_balances_revenue_to_fee_test() { 33928060 + 3600 ); - assert!(update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).is_err()); // now timestamp passed is wrong - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now + 3600).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + assert!(update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now + ) + .is_err()); // now timestamp passed is wrong + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now + 3600, + ) + .unwrap(); assert_eq!(market.insurance_claim.last_revenue_withdraw_ts, 33931660); assert_eq!(spot_market.insurance_fund.last_revenue_settle_ts, 33931660); @@ -1075,7 +1261,15 @@ fn update_pool_balances_revenue_to_fee_devnet_state_test() { let prev_rev_pool = spot_market.revenue_pool.scaled_balance; let prev_tfmd = market.amm.total_fee_minus_distributions; - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 1821000000000); assert_eq!(market.pnl_pool.scaled_balance, 381047000000000); @@ -1166,7 +1360,15 @@ fn update_pool_balances_revenue_to_fee_new_market() { let prev_rev_pool = spot_market.revenue_pool.scaled_balance; // let prev_tfmd = market.amm.total_fee_minus_distributions; - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 50000000000); // $50 @@ -1512,10 +1714,11 @@ mod revenue_pool_transfer_tests { let spot_position = SpotPosition::default(); let unsettled_pnl = -100; let now = 100; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -1527,10 +1730,11 @@ mod revenue_pool_transfer_tests { // revenue pool not yet settled let now = 10000; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -1545,10 +1749,11 @@ mod revenue_pool_transfer_tests { market.amm.net_revenue_since_last_funding = -169; let now = 10000; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -1563,10 +1768,11 @@ mod revenue_pool_transfer_tests { market.amm.net_revenue_since_last_funding = 169; let now = 10000; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -1629,10 +1835,11 @@ mod revenue_pool_transfer_tests { let spot_position = SpotPosition::default(); let unsettled_pnl = -100; let now = 100; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -1644,10 +1851,11 @@ mod revenue_pool_transfer_tests { // revenue pool not yet settled let now = 10000; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -1662,10 +1870,11 @@ mod revenue_pool_transfer_tests { market.amm.net_revenue_since_last_funding = -169; let now = 10000; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) diff --git a/programs/drift/src/controller/isolated_position.rs b/programs/drift/src/controller/isolated_position.rs new file mode 100644 index 0000000000..5bf940354b --- /dev/null +++ b/programs/drift/src/controller/isolated_position.rs @@ -0,0 +1,416 @@ +use std::cell::RefMut; + +use crate::controller; +use crate::controller::spot_balance::update_spot_balances; +use crate::controller::spot_position::update_spot_balances_and_cumulative_deposits; +use crate::error::{DriftResult, ErrorCode}; +use crate::get_then_update_id; +use crate::math::casting::Cast; +use crate::math::liquidation::is_isolated_margin_being_liquidated; +use crate::math::margin::{validate_spot_margin_trading, MarginRequirementType}; +use crate::state::events::{DepositDirection, DepositExplanation, DepositRecord}; +use crate::state::oracle_map::OracleMap; +use crate::state::perp_market::MarketStatus; +use crate::state::perp_market_map::PerpMarketMap; +use crate::state::spot_market::SpotBalanceType; +use crate::state::spot_market_map::SpotMarketMap; +use crate::state::state::State; +use crate::state::user::{User, UserStats}; +use crate::validate; +use anchor_lang::prelude::*; + +#[cfg(test)] +mod tests; + +pub fn deposit_into_isolated_perp_position<'c: 'info, 'info>( + user_key: Pubkey, + user: &mut User, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + slot: u64, + now: i64, + state: &State, + spot_market_index: u16, + perp_market_index: u16, + amount: u64, +) -> DriftResult<()> { + validate!( + amount != 0, + ErrorCode::InsufficientDeposit, + "deposit amount cant be 0", + )?; + + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + + let perp_market = perp_market_map.get_ref(&perp_market_index)?; + + validate!( + perp_market.quote_spot_market_index == spot_market_index, + ErrorCode::InvalidIsolatedPerpMarket, + "perp market quote spot market index ({}) != spot market index ({})", + perp_market.quote_spot_market_index, + spot_market_index + )?; + + let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; + let oracle_price_data = *oracle_map.get_price_data(&spot_market.oracle_id())?; + + validate!( + user.pool_id == spot_market.pool_id, + ErrorCode::InvalidPoolId, + "user pool id ({}) != market pool id ({})", + user.pool_id, + spot_market.pool_id + )?; + + validate!( + !matches!(spot_market.status, MarketStatus::Initialized), + ErrorCode::MarketBeingInitialized, + "Market is being initialized" + )?; + + controller::spot_balance::update_spot_market_cumulative_interest( + &mut spot_market, + Some(&oracle_price_data), + now, + )?; + + user.increment_total_deposits( + amount, + oracle_price_data.price, + spot_market.get_precision().cast()?, + )?; + + let total_deposits_after = user.total_deposits; + let total_withdraws_after = user.total_withdraws; + + { + let perp_position = user.force_get_isolated_perp_position_mut(perp_market_index)?; + + update_spot_balances( + amount.cast::()?, + &SpotBalanceType::Deposit, + &mut spot_market, + perp_position, + false, + )?; + } + + validate!( + matches!(spot_market.status, MarketStatus::Active), + ErrorCode::MarketActionPaused, + "spot_market not active", + )?; + + drop(spot_market); + + if user.is_isolated_margin_being_liquidated(perp_market_index)? { + // try to update liquidation status if user is was already being liq'd + let is_being_liquidated = is_isolated_margin_being_liquidated( + user, + perp_market_map, + spot_market_map, + oracle_map, + perp_market_index, + state.liquidation_margin_buffer_ratio, + )?; + + if !is_being_liquidated { + user.exit_isolated_margin_liquidation(perp_market_index)?; + } + } + + user.update_last_active_slot(slot); + + let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; + + let deposit_record_id = get_then_update_id!(spot_market, next_deposit_record_id); + let oracle_price = oracle_price_data.price; + + let deposit_record = DepositRecord { + ts: now, + deposit_record_id, + user_authority: user.authority, + user: user_key, + direction: DepositDirection::Deposit, + amount, + oracle_price, + market_deposit_balance: spot_market.deposit_balance, + market_withdraw_balance: spot_market.borrow_balance, + market_cumulative_deposit_interest: spot_market.cumulative_deposit_interest, + market_cumulative_borrow_interest: spot_market.cumulative_borrow_interest, + total_deposits_after, + total_withdraws_after, + market_index: spot_market_index, + explanation: DepositExplanation::None, + transfer_user: None, + }; + + emit!(deposit_record); + + Ok(()) +} + +pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( + user: &mut User, + user_stats: &mut UserStats, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + slot: u64, + now: i64, + spot_market_index: u16, + perp_market_index: u16, + amount: i64, +) -> DriftResult<()> { + validate!( + amount != 0, + ErrorCode::DefaultError, + "transfer amount cant be 0", + )?; + + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + + { + let perp_market = &perp_market_map.get_ref(&perp_market_index)?; + let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; + + validate!( + perp_market.quote_spot_market_index == spot_market_index, + ErrorCode::InvalidIsolatedPerpMarket, + "perp market quote spot market index ({}) != spot market index ({})", + perp_market.quote_spot_market_index, + spot_market_index + )?; + + validate!( + user.pool_id == spot_market.pool_id && user.pool_id == perp_market.pool_id, + ErrorCode::InvalidPoolId, + "user pool id ({}) != market pool id ({})", + user.pool_id, + spot_market.pool_id + )?; + + let oracle_price_data = oracle_map.get_price_data(&spot_market.oracle_id())?; + controller::spot_balance::update_spot_market_cumulative_interest( + spot_market, + Some(oracle_price_data), + now, + )?; + } + + if amount > 0 { + let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; + + let spot_position_index = user.force_get_spot_position_index(spot_market.market_index)?; + update_spot_balances_and_cumulative_deposits( + amount as u128, + &SpotBalanceType::Borrow, + &mut spot_market, + &mut user.spot_positions[spot_position_index], + false, + None, + )?; + + update_spot_balances( + amount as u128, + &SpotBalanceType::Deposit, + &mut spot_market, + user.force_get_isolated_perp_position_mut(perp_market_index)?, + false, + )?; + + drop(spot_market); + + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + oracle_map, + MarginRequirementType::Initial, + spot_market_index, + amount as u128, + user_stats, + now, + )?; + + validate_spot_margin_trading(user, &perp_market_map, &spot_market_map, oracle_map)?; + + if user.is_cross_margin_being_liquidated() { + user.exit_cross_margin_liquidation(); + } + + if user.is_isolated_margin_being_liquidated(perp_market_index)? { + user.exit_isolated_margin_liquidation(perp_market_index)?; + } + } else { + let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; + + let isolated_perp_position_token_amount = user + .force_get_isolated_perp_position_mut(perp_market_index)? + .get_isolated_token_amount(&spot_market)?; + + validate!( + amount.unsigned_abs() as u128 <= isolated_perp_position_token_amount, + ErrorCode::InsufficientCollateral, + "user has insufficient deposit for market {}", + spot_market_index + )?; + + let spot_position_index = user.force_get_spot_position_index(spot_market.market_index)?; + update_spot_balances_and_cumulative_deposits( + amount.abs() as u128, + &SpotBalanceType::Deposit, + &mut spot_market, + &mut user.spot_positions[spot_position_index], + false, + None, + )?; + + update_spot_balances( + amount.abs() as u128, + &SpotBalanceType::Borrow, + &mut spot_market, + user.force_get_isolated_perp_position_mut(perp_market_index)?, + false, + )?; + + drop(spot_market); + + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + oracle_map, + MarginRequirementType::Initial, + 0, + 0, + user_stats, + now, + )?; + + if user.is_isolated_margin_being_liquidated(perp_market_index)? { + user.exit_isolated_margin_liquidation(perp_market_index)?; + } + + if user.is_cross_margin_being_liquidated() { + user.exit_cross_margin_liquidation(); + } + } + + user.update_last_active_slot(slot); + + Ok(()) +} + +pub fn withdraw_from_isolated_perp_position<'c: 'info, 'info>( + user_key: Pubkey, + user: &mut User, + user_stats: &mut UserStats, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + slot: u64, + now: i64, + spot_market_index: u16, + perp_market_index: u16, + amount: u64, +) -> DriftResult<()> { + validate!( + amount != 0, + ErrorCode::DefaultError, + "withdraw amount cant be 0", + )?; + + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + + { + let perp_market = &perp_market_map.get_ref(&perp_market_index)?; + + validate!( + perp_market.quote_spot_market_index == spot_market_index, + ErrorCode::InvalidIsolatedPerpMarket, + "perp market quote spot market index ({}) != spot market index ({})", + perp_market.quote_spot_market_index, + spot_market_index + )?; + + let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; + let oracle_price_data = oracle_map.get_price_data(&spot_market.oracle_id())?; + + controller::spot_balance::update_spot_market_cumulative_interest( + spot_market, + Some(oracle_price_data), + now, + )?; + + user.increment_total_withdraws( + amount, + oracle_price_data.price, + spot_market.get_precision().cast()?, + )?; + + let isolated_perp_position = + user.force_get_isolated_perp_position_mut(perp_market_index)?; + + let isolated_position_token_amount = + isolated_perp_position.get_isolated_token_amount(spot_market)?; + + validate!( + amount as u128 <= isolated_position_token_amount, + ErrorCode::InsufficientCollateral, + "user has insufficient deposit for market {}", + spot_market_index + )?; + + update_spot_balances( + amount as u128, + &SpotBalanceType::Borrow, + spot_market, + isolated_perp_position, + true, + )?; + } + + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + oracle_map, + MarginRequirementType::Initial, + 0, + 0, + user_stats, + now, + )?; + + if user.is_isolated_margin_being_liquidated(perp_market_index)? { + user.exit_isolated_margin_liquidation(perp_market_index)?; + } + + user.update_last_active_slot(slot); + + let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; + let oracle_price = oracle_map.get_price_data(&spot_market.oracle_id())?.price; + + let deposit_record_id = get_then_update_id!(spot_market, next_deposit_record_id); + let deposit_record = DepositRecord { + ts: now, + deposit_record_id, + user_authority: user.authority, + user: user_key, + direction: DepositDirection::Withdraw, + oracle_price, + amount, + market_index: spot_market_index, + market_deposit_balance: spot_market.deposit_balance, + market_withdraw_balance: spot_market.borrow_balance, + market_cumulative_deposit_interest: spot_market.cumulative_deposit_interest, + market_cumulative_borrow_interest: spot_market.cumulative_borrow_interest, + total_deposits_after: user.total_deposits, + total_withdraws_after: user.total_withdraws, + explanation: DepositExplanation::None, + transfer_user: None, + }; + emit!(deposit_record); + + Ok(()) +} diff --git a/programs/drift/src/controller/isolated_position/tests.rs b/programs/drift/src/controller/isolated_position/tests.rs new file mode 100644 index 0000000000..2c2cac1660 --- /dev/null +++ b/programs/drift/src/controller/isolated_position/tests.rs @@ -0,0 +1,1124 @@ +pub mod deposit_into_isolated_perp_position { + use crate::controller::isolated_position::deposit_into_isolated_perp_position; + use crate::error::ErrorCode; + use crate::state::state::State; + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I128, LIQUIDATION_FEE_PRECISION, PEG_PRECISION, + QUOTE_PRECISION_I128, QUOTE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, + SPOT_WEIGHT_PRECISION, + }; + 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::SpotMarket; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::user::{PerpPosition, PositionFlag, User}; + use crate::test_utils::get_pyth_price; + use crate::{create_account_info, PRICE_PRECISION_I64}; + use crate::{create_anchor_account_info, test_utils::*}; + + #[test] + pub fn successful_deposit_into_isolated_perp_position() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..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 spot_market = SpotMarket { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User::default(); + + let user_key = Pubkey::default(); + + let state = State::default(); + deposit_into_isolated_perp_position( + user_key, + &mut user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + &state, + 0, + 0, + QUOTE_PRECISION_U64, + ) + .unwrap(); + + assert_eq!( + user.perp_positions[0].isolated_position_scaled_balance, + 1000000000 + ); + assert_eq!( + user.perp_positions[0].position_flag, + PositionFlag::IsolatedPosition as u8 + ); + } + + #[test] + pub fn fail_to_deposit_into_existing_perp_position() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..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 spot_market = SpotMarket { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User::default(); + user.perp_positions[0] = PerpPosition { + market_index: 0, + open_orders: 1, + ..PerpPosition::default() + }; + + let user_key = Pubkey::default(); + + let state = State::default(); + let result = deposit_into_isolated_perp_position( + user_key, + &mut user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + &state, + 0, + 0, + QUOTE_PRECISION_U64, + ); + + assert_eq!(result, Err(ErrorCode::InvalidPerpPosition)); + } +} + +pub mod transfer_isolated_perp_position_deposit { + use crate::controller::isolated_position::transfer_isolated_perp_position_deposit; + use crate::error::ErrorCode; + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I128, LIQUIDATION_FEE_PRECISION, PEG_PRECISION, + QUOTE_PRECISION_I128, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, + }; + 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::SpotMarket; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::user::{PerpPosition, PositionFlag, SpotPosition, User, UserStats}; + use crate::test_utils::get_pyth_price; + use crate::{create_account_info, PRICE_PRECISION_I64}; + use crate::{ + create_anchor_account_info, test_utils::*, QUOTE_PRECISION_I64, SPOT_BALANCE_PRECISION, + SPOT_BALANCE_PRECISION_U64, + }; + + #[test] + pub fn successful_transfer_to_isolated_perp_position() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..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 spot_market = SpotMarket { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User::default(); + user.spot_positions[0] = SpotPosition { + market_index: 0, + scaled_balance: SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + + let mut user_stats = UserStats::default(); + + transfer_isolated_perp_position_deposit( + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + QUOTE_PRECISION_I64, + ) + .unwrap(); + + assert_eq!( + user.perp_positions[0].isolated_position_scaled_balance, + 1000000000 + ); + assert_eq!( + user.perp_positions[0].position_flag, + PositionFlag::IsolatedPosition as u8 + ); + + assert_eq!(user.spot_positions[0].scaled_balance, 0); + } + + #[test] + pub fn fail_to_transfer_to_existing_perp_position() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..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 spot_market = SpotMarket { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User::default(); + user.spot_positions[0] = SpotPosition { + market_index: 0, + scaled_balance: SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + user.perp_positions[0] = PerpPosition { + market_index: 0, + open_orders: 1, + ..PerpPosition::default() + }; + + let mut user_stats = UserStats::default(); + + let result = transfer_isolated_perp_position_deposit( + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + QUOTE_PRECISION_I64, + ); + + assert_eq!(result, Err(ErrorCode::InvalidPerpPosition)); + } + + #[test] + pub fn fail_to_transfer_due_to_insufficient_collateral() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..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 spot_market = SpotMarket { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 2 * SPOT_BALANCE_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User::default(); + user.spot_positions[0] = SpotPosition { + market_index: 0, + scaled_balance: SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + + let mut user_stats = UserStats::default(); + + let result = transfer_isolated_perp_position_deposit( + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + 2 * QUOTE_PRECISION_I64, + ); + + assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); + } + + #[test] + pub fn successful_transfer_from_isolated_perp_position() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..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 spot_market = SpotMarket { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User::default(); + user.perp_positions[0] = PerpPosition { + market_index: 0, + isolated_position_scaled_balance: SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }; + + let mut user_stats = UserStats::default(); + + transfer_isolated_perp_position_deposit( + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + -QUOTE_PRECISION_I64, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 0); + assert_eq!( + user.perp_positions[0].position_flag, + PositionFlag::IsolatedPosition as u8 + ); + + assert_eq!( + user.spot_positions[0].scaled_balance, + SPOT_BALANCE_PRECISION_U64 + ); + } + + #[test] + pub fn fail_transfer_from_non_isolated_perp_position() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..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 spot_market = SpotMarket { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User::default(); + user.perp_positions[0] = PerpPosition { + market_index: 0, + open_orders: 1, + ..PerpPosition::default() + }; + + let mut user_stats = UserStats::default(); + + let result = transfer_isolated_perp_position_deposit( + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + -QUOTE_PRECISION_I64, + ); + + assert_eq!(result, Err(ErrorCode::InvalidPerpPosition)); + } + + #[test] + pub fn fail_transfer_from_isolated_perp_position_due_to_insufficient_collateral() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..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 spot_market = SpotMarket { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User::default(); + user.perp_positions[0] = PerpPosition { + market_index: 0, + base_asset_amount: 100000, + isolated_position_scaled_balance: SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }; + + let mut user_stats = UserStats::default(); + + let result = transfer_isolated_perp_position_deposit( + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + -QUOTE_PRECISION_I64, + ); + + assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); + } +} + +pub mod withdraw_from_isolated_perp_position { + use crate::controller::isolated_position::withdraw_from_isolated_perp_position; + use crate::error::ErrorCode; + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I128, LIQUIDATION_FEE_PRECISION, PEG_PRECISION, + QUOTE_PRECISION_I128, QUOTE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, + SPOT_WEIGHT_PRECISION, + }; + 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::SpotMarket; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::user::{PerpPosition, PositionFlag, User, UserStats}; + use crate::test_utils::get_pyth_price; + use crate::{create_account_info, PRICE_PRECISION_I64}; + use crate::{ + create_anchor_account_info, test_utils::*, SPOT_BALANCE_PRECISION, + SPOT_BALANCE_PRECISION_U64, + }; + + #[test] + pub fn successful_withdraw_from_isolated_perp_position() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..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 spot_market = SpotMarket { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User::default(); + user.perp_positions[0] = PerpPosition { + market_index: 0, + isolated_position_scaled_balance: SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }; + + let user_key = Pubkey::default(); + + let mut user_stats = UserStats::default(); + + withdraw_from_isolated_perp_position( + user_key, + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + QUOTE_PRECISION_U64, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 0); + assert_eq!( + user.perp_positions[0].position_flag, + PositionFlag::IsolatedPosition as u8 + ); + } + + #[test] + pub fn withdraw_from_isolated_perp_position_fail_not_isolated_perp_position() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..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 spot_market = SpotMarket { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User::default(); + user.perp_positions[0] = PerpPosition { + market_index: 0, + open_orders: 1, + ..PerpPosition::default() + }; + + let user_key = Pubkey::default(); + + let mut user_stats = UserStats::default(); + + let result = withdraw_from_isolated_perp_position( + user_key, + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + QUOTE_PRECISION_U64, + ); + + assert_eq!(result, Err(ErrorCode::InvalidPerpPosition)); + } + + #[test] + pub fn fail_withdraw_from_isolated_perp_position_due_to_insufficient_collateral() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..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 spot_market = SpotMarket { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User::default(); + user.perp_positions[0] = PerpPosition { + market_index: 0, + base_asset_amount: 100000, + isolated_position_scaled_balance: SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }; + + let user_key = Pubkey::default(); + + let mut user_stats = UserStats::default(); + + let result = withdraw_from_isolated_perp_position( + user_key, + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + QUOTE_PRECISION_U64, + ); + + assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); + } +} diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 9503f24966..f9d70a3b90 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -1,6 +1,9 @@ use std::ops::{Deref, DerefMut}; use crate::msg; +use crate::state::liquidation_mode::{ + get_perp_liquidation_mode, CrossMarginLiquidatePerpMode, LiquidatePerpMode, +}; use anchor_lang::prelude::*; use crate::controller::amm::get_fee_pool_tokens; @@ -18,7 +21,7 @@ use crate::controller::spot_balance::{ }; use crate::controller::spot_position::update_spot_balances_and_cumulative_deposits; use crate::error::{DriftResult, ErrorCode}; -use crate::math::bankruptcy::is_user_bankrupt; +use crate::math::bankruptcy::is_cross_margin_bankrupt; use crate::math::casting::Cast; use crate::math::constants::{ LIQUIDATION_FEE_PRECISION_U128, LIQUIDATION_PCT_PRECISION, QUOTE_PRECISION, @@ -93,8 +96,10 @@ pub fn liquidate_perp( let initial_pct_to_liquidate = state.initial_pct_to_liquidate as u128; let liquidation_duration = state.liquidation_duration as u128; + let liquidation_mode = get_perp_liquidation_mode(&user, market_index)?; + validate!( - !user.is_bankrupt(), + !liquidation_mode.is_user_bankrupt(&user)?, ErrorCode::UserBankrupt, "user bankrupt", )?; @@ -146,11 +151,16 @@ pub fn liquidate_perp( .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; - if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { + let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(&user)?; + if !user_is_being_liquidated + && liquidation_mode.meets_margin_requirements(&margin_calculation)? + { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_being_liquidated() && margin_calculation.can_exit_liquidation()? { - user.exit_liquidation(); + } else if user_is_being_liquidated + && liquidation_mode.can_exit_liquidation(&margin_calculation)? + { + liquidation_mode.exit_liquidation(user)?; return Ok(()); } @@ -172,7 +182,7 @@ pub fn liquidate_perp( e })?; - let liquidation_id = user.enter_liquidation(slot)?; + let liquidation_id = liquidation_mode.enter_liquidation(user, slot)?; let mut margin_freed = 0_u64; let position_index = get_position_index(&user.perp_positions, market_index)?; @@ -182,6 +192,8 @@ pub fn liquidate_perp( ErrorCode::PositionDoesntHaveOpenPositionOrOrders )?; + let (cancel_order_market_type, cancel_order_market_index) = + liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( user, user_key, @@ -192,9 +204,10 @@ pub fn liquidate_perp( now, slot, OrderActionExplanation::Liquidation, + cancel_order_market_type, + cancel_order_market_index, None, - None, - None, + true, )?; let mut market = perp_market_map.get_ref_mut(&market_index)?; @@ -222,11 +235,8 @@ pub fn liquidate_perp( drop(market); - // burning lp shares = removing open bids/asks - let lp_shares = 0; - // check if user exited liquidation territory - let intermediate_margin_calculation = if !canceled_order_ids.is_empty() || lp_shares > 0 { + let intermediate_margin_calculation = if !canceled_order_ids.is_empty() { let intermediate_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, @@ -237,42 +247,46 @@ pub fn liquidate_perp( .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; - let initial_margin_shortage = margin_calculation.margin_shortage()?; - let new_margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let initial_margin_shortage = liquidation_mode.margin_shortage(&margin_calculation)?; + let new_margin_shortage = + liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) .cast::()?; - user.increment_margin_freed(margin_freed)?; + liquidation_mode.increment_free_margin(user, margin_freed)?; - if intermediate_margin_calculation.can_exit_liquidation()? { + if liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)? { + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, liquidation_type: LiquidationType::LiquidatePerp, user: *user_key, liquidator: *liquidator_key, - margin_requirement: margin_calculation.margin_requirement, - total_collateral: margin_calculation.total_collateral, - bankrupt: user.is_bankrupt(), + margin_requirement, + total_collateral, + bankrupt: liquidation_mode.is_user_bankrupt(user)?, canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { market_index, oracle_price, - lp_shares, + lp_shares: 0, ..LiquidatePerpRecord::default() }, + bit_flags, ..LiquidationRecord::default() }); - user.exit_liquidation(); + liquidation_mode.exit_liquidation(user)?; return Ok(()); } intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; if user.perp_positions[position_index].base_asset_amount == 0 { @@ -323,7 +337,7 @@ pub fn liquidate_perp( let margin_ratio_with_buffer = margin_ratio.safe_add(liquidation_margin_buffer_ratio)?; - let margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let margin_shortage = liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; let market = perp_market_map.get_ref(&market_index)?; let quote_spot_market = spot_market_map.get_ref(&market.quote_spot_market_index)?; @@ -370,7 +384,7 @@ pub fn liquidate_perp( drop(market); drop(quote_spot_market); - let max_pct_allowed = calculate_max_pct_to_liquidate( + let max_pct_allowed = liquidation_mode.calculate_max_pct_to_liquidate( user, margin_shortage, slot, @@ -550,14 +564,15 @@ pub fn liquidate_perp( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + Some(liquidation_mode.as_ref()), )?; margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; - user.increment_margin_freed(margin_freed_for_perp_position)?; + liquidation_mode.increment_free_margin(user, margin_freed_for_perp_position)?; if base_asset_amount >= base_asset_amount_to_cover_margin_shortage { - user.exit_liquidation(); - } else if is_user_bankrupt(user) { - user.enter_bankruptcy(); + liquidation_mode.exit_liquidation(user)?; + } else if liquidation_mode.should_user_enter_bankruptcy(user)? { + liquidation_mode.enter_bankruptcy(user)?; } let liquidator_meets_initial_margin_requirement = @@ -680,15 +695,17 @@ pub fn liquidate_perp( }; emit!(fill_record); + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, liquidation_type: LiquidationType::LiquidatePerp, user: *user_key, liquidator: *liquidator_key, - margin_requirement: margin_calculation.margin_requirement, - total_collateral: margin_calculation.total_collateral, - bankrupt: user.is_bankrupt(), + margin_requirement, + total_collateral, + bankrupt: liquidation_mode.is_user_bankrupt(&user)?, canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { @@ -696,13 +713,14 @@ pub fn liquidate_perp( oracle_price, base_asset_amount: user_position_delta.base_asset_amount, quote_asset_amount: user_position_delta.quote_asset_amount, - lp_shares, + lp_shares: 0, user_order_id, liquidator_order_id, fill_record_id, liquidator_fee: liquidator_fee.abs().cast()?, if_fee: if_fee.abs().cast()?, }, + bit_flags, ..LiquidationRecord::default() }); @@ -735,8 +753,10 @@ pub fn liquidate_perp_with_fill( let initial_pct_to_liquidate = state.initial_pct_to_liquidate as u128; let liquidation_duration = state.liquidation_duration as u128; + let liquidation_mode = get_perp_liquidation_mode(&user, market_index)?; + validate!( - !user.is_bankrupt(), + !liquidation_mode.is_user_bankrupt(&user)?, ErrorCode::UserBankrupt, "user bankrupt", )?; @@ -788,11 +808,16 @@ pub fn liquidate_perp_with_fill( .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; - if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { + let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(&user)?; + if !user_is_being_liquidated + && liquidation_mode.meets_margin_requirements(&margin_calculation)? + { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_being_liquidated() && margin_calculation.can_exit_liquidation()? { - user.exit_liquidation(); + } else if user_is_being_liquidated + && liquidation_mode.can_exit_liquidation(&margin_calculation)? + { + liquidation_mode.exit_liquidation(&mut user)?; return Ok(()); } @@ -804,7 +829,7 @@ pub fn liquidate_perp_with_fill( e })?; - let liquidation_id = user.enter_liquidation(slot)?; + let liquidation_id = liquidation_mode.enter_liquidation(&mut user, slot)?; let mut margin_freed = 0_u64; let position_index = get_position_index(&user.perp_positions, market_index)?; @@ -814,6 +839,8 @@ pub fn liquidate_perp_with_fill( ErrorCode::PositionDoesntHaveOpenPositionOrOrders )?; + let (cancel_orders_market_type, cancel_orders_market_index) = + liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( &mut user, user_key, @@ -824,9 +851,10 @@ pub fn liquidate_perp_with_fill( now, slot, OrderActionExplanation::Liquidation, + cancel_orders_market_type, + cancel_orders_market_index, None, - None, - None, + true, )?; let mut market = perp_market_map.get_ref_mut(&market_index)?; @@ -854,11 +882,8 @@ pub fn liquidate_perp_with_fill( drop(market); - // burning lp shares = removing open bids/asks - let lp_shares = 0; - // check if user exited liquidation territory - let intermediate_margin_calculation = if !canceled_order_ids.is_empty() || lp_shares > 0 { + let intermediate_margin_calculation = if !canceled_order_ids.is_empty() { let intermediate_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &user, @@ -869,42 +894,46 @@ pub fn liquidate_perp_with_fill( .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; - let initial_margin_shortage = margin_calculation.margin_shortage()?; - let new_margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let initial_margin_shortage = liquidation_mode.margin_shortage(&margin_calculation)?; + let new_margin_shortage = + liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) .cast::()?; - user.increment_margin_freed(margin_freed)?; + liquidation_mode.increment_free_margin(&mut user, margin_freed)?; - if intermediate_margin_calculation.can_exit_liquidation()? { + if liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)? { + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, liquidation_type: LiquidationType::LiquidatePerp, user: *user_key, liquidator: *liquidator_key, - margin_requirement: margin_calculation.margin_requirement, - total_collateral: margin_calculation.total_collateral, - bankrupt: user.is_bankrupt(), + margin_requirement, + total_collateral, + bankrupt: liquidation_mode.is_user_bankrupt(&user)?, canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { market_index, oracle_price, - lp_shares, + lp_shares: 0, ..LiquidatePerpRecord::default() }, + bit_flags, ..LiquidationRecord::default() }); - user.exit_liquidation(); + liquidation_mode.exit_liquidation(&mut user)?; return Ok(()); } intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; if user.perp_positions[position_index].base_asset_amount == 0 { @@ -939,7 +968,7 @@ pub fn liquidate_perp_with_fill( let margin_ratio_with_buffer = margin_ratio.safe_add(liquidation_margin_buffer_ratio)?; - let margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let margin_shortage = liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; let market = perp_market_map.get_ref(&market_index)?; let quote_spot_market = spot_market_map.get_ref(&market.quote_spot_market_index)?; @@ -970,7 +999,7 @@ pub fn liquidate_perp_with_fill( drop(market); drop(quote_spot_market); - let max_pct_allowed = calculate_max_pct_to_liquidate( + let max_pct_allowed = liquidation_mode.calculate_max_pct_to_liquidate( &user, margin_shortage, slot, @@ -1114,15 +1143,16 @@ pub fn liquidate_perp_with_fill( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + Some(liquidation_mode.as_ref()), )?; margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; - user.increment_margin_freed(margin_freed_for_perp_position)?; + liquidation_mode.increment_free_margin(&mut user, margin_freed_for_perp_position)?; - if margin_calculation_after.meets_margin_requirement() { - user.exit_liquidation(); - } else if is_user_bankrupt(&user) { - user.enter_bankruptcy(); + if liquidation_mode.can_exit_liquidation(&margin_calculation_after)? { + liquidation_mode.exit_liquidation(&mut user)?; + } else if liquidation_mode.should_user_enter_bankruptcy(&user)? { + liquidation_mode.enter_bankruptcy(&mut user)?; } let user_position_delta = get_position_delta_for_fill( @@ -1131,15 +1161,17 @@ pub fn liquidate_perp_with_fill( existing_direction, )?; + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, liquidation_type: LiquidationType::LiquidatePerp, user: *user_key, liquidator: *liquidator_key, - margin_requirement: margin_calculation.margin_requirement, - total_collateral: margin_calculation.total_collateral, - bankrupt: user.is_bankrupt(), + margin_requirement, + total_collateral, + bankrupt: liquidation_mode.is_user_bankrupt(&user)?, canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { @@ -1147,13 +1179,14 @@ pub fn liquidate_perp_with_fill( oracle_price, base_asset_amount: user_position_delta.base_asset_amount, quote_asset_amount: user_position_delta.quote_asset_amount, - lp_shares, + lp_shares: 0, user_order_id: order_id, liquidator_order_id: 0, fill_record_id, liquidator_fee: 0, if_fee: if_fee.abs().cast()?, }, + bit_flags, ..LiquidationRecord::default() }); @@ -1183,7 +1216,7 @@ pub fn liquidate_spot( let liquidation_duration = state.liquidation_duration as u128; validate!( - !user.is_bankrupt(), + !user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "user bankrupt", )?; @@ -1394,15 +1427,19 @@ pub fn liquidate_spot( now, )?; - if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { + if !user.is_cross_margin_being_liquidated() + && margin_calculation.meets_cross_margin_requirement() + { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_being_liquidated() && margin_calculation.can_exit_liquidation()? { - user.exit_liquidation(); + } else if user.is_cross_margin_being_liquidated() + && margin_calculation.can_exit_cross_margin_liquidation()? + { + user.exit_cross_margin_liquidation(); return Ok(()); } - let liquidation_id = user.enter_liquidation(slot)?; + let liquidation_id = user.enter_cross_margin_liquidation(slot)?; let mut margin_freed = 0_u64; let canceled_order_ids = orders::cancel_orders( @@ -1418,6 +1455,7 @@ pub fn liquidate_spot( None, None, None, + true, )?; // check if user exited liquidation territory @@ -1435,15 +1473,15 @@ pub fn liquidate_spot( .fuel_numerator(user, now), )?; - let initial_margin_shortage = margin_calculation.margin_shortage()?; - let new_margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let initial_margin_shortage = margin_calculation.cross_margin_margin_shortage()?; + let new_margin_shortage = intermediate_margin_calculation.cross_margin_margin_shortage()?; margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) .cast::()?; user.increment_margin_freed(margin_freed)?; - if intermediate_margin_calculation.can_exit_liquidation()? { + if intermediate_margin_calculation.can_exit_cross_margin_liquidation()? { emit!(LiquidationRecord { ts: now, liquidation_id, @@ -1452,7 +1490,7 @@ pub fn liquidate_spot( liquidator: *liquidator_key, margin_requirement: margin_calculation.margin_requirement, total_collateral: margin_calculation.total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), canceled_order_ids, margin_freed, liquidate_spot: LiquidateSpotRecord { @@ -1467,16 +1505,16 @@ pub fn liquidate_spot( ..LiquidationRecord::default() }); - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); return Ok(()); } intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; - let margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let margin_shortage = intermediate_margin_calculation.cross_margin_margin_shortage()?; let liability_weight_with_buffer = liability_weight.safe_add(liquidation_margin_buffer_ratio)?; @@ -1682,14 +1720,15 @@ pub fn liquidate_spot( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + None, )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; user.increment_margin_freed(margin_freed_from_liability)?; if liability_transfer >= liability_transfer_to_cover_margin_shortage { - user.exit_liquidation(); - } else if is_user_bankrupt(user) { - user.enter_bankruptcy(); + user.exit_cross_margin_liquidation(); + } else if is_cross_margin_bankrupt(user) { + user.enter_cross_margin_bankruptcy(); } let liq_margin_context = MarginContext::standard(MarginRequirementType::Initial) @@ -1724,7 +1763,7 @@ pub fn liquidate_spot( liquidator: *liquidator_key, margin_requirement: margin_calculation.margin_requirement, total_collateral: margin_calculation.total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), margin_freed, liquidate_spot: LiquidateSpotRecord { asset_market_index, @@ -1763,7 +1802,7 @@ pub fn liquidate_spot_with_swap_begin( let liquidation_duration = state.liquidation_duration as u128; validate!( - !user.is_bankrupt(), + !user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "user bankrupt", )?; @@ -1923,15 +1962,19 @@ pub fn liquidate_spot_with_swap_begin( now, )?; - if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { + if !user.is_cross_margin_being_liquidated() + && margin_calculation.meets_cross_margin_requirement() + { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_being_liquidated() && margin_calculation.can_exit_liquidation()? { + } else if user.is_cross_margin_being_liquidated() + && margin_calculation.can_exit_cross_margin_liquidation()? + { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::InvalidLiquidation); } - let liquidation_id = user.enter_liquidation(slot)?; + let liquidation_id = user.enter_cross_margin_liquidation(slot)?; let canceled_order_ids = orders::cancel_orders( user, @@ -1946,6 +1989,7 @@ pub fn liquidate_spot_with_swap_begin( None, None, None, + true, )?; // check if user exited liquidation territory @@ -1963,8 +2007,8 @@ pub fn liquidate_spot_with_swap_begin( .fuel_numerator(user, now), )?; - let initial_margin_shortage = margin_calculation.margin_shortage()?; - let new_margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let initial_margin_shortage = margin_calculation.cross_margin_margin_shortage()?; + let new_margin_shortage = intermediate_margin_calculation.cross_margin_margin_shortage()?; let margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) @@ -1979,7 +2023,7 @@ pub fn liquidate_spot_with_swap_begin( liquidator: *liquidator_key, margin_requirement: margin_calculation.margin_requirement, total_collateral: margin_calculation.total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), canceled_order_ids, margin_freed, liquidate_spot: LiquidateSpotRecord { @@ -1995,16 +2039,16 @@ pub fn liquidate_spot_with_swap_begin( }); // must throw error to stop swap - if intermediate_margin_calculation.can_exit_liquidation()? { + if intermediate_margin_calculation.can_exit_cross_margin_liquidation()? { return Err(ErrorCode::InvalidLiquidation); } intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; - let margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let margin_shortage = intermediate_margin_calculation.cross_margin_margin_shortage()?; let liability_weight_with_buffer = liability_weight.safe_add(liquidation_margin_buffer_ratio)?; @@ -2208,10 +2252,10 @@ pub fn liquidate_spot_with_swap_end( now, )?; - let liquidation_id = user.enter_liquidation(slot)?; + let liquidation_id = user.enter_cross_margin_liquidation(slot)?; let mut margin_freed = 0_u64; - let margin_shortage = margin_calculation.margin_shortage()?; + let margin_shortage = margin_calculation.cross_margin_margin_shortage()?; let if_fee = liability_transfer .cast::()? @@ -2252,15 +2296,16 @@ pub fn liquidate_spot_with_swap_end( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + None, )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; user.increment_margin_freed(margin_freed_from_liability)?; - if margin_calulcation_after.can_exit_liquidation()? { - user.exit_liquidation(); - } else if is_user_bankrupt(user) { - user.enter_bankruptcy(); + if margin_calulcation_after.can_exit_cross_margin_liquidation()? { + user.exit_cross_margin_liquidation(); + } else if is_cross_margin_bankrupt(user) { + user.enter_cross_margin_bankruptcy(); } emit!(LiquidationRecord { @@ -2271,7 +2316,7 @@ pub fn liquidate_spot_with_swap_end( liquidator: *liquidator_key, margin_requirement: margin_calculation.margin_requirement, total_collateral: margin_calculation.total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), margin_freed, liquidate_spot: LiquidateSpotRecord { asset_market_index, @@ -2311,7 +2356,7 @@ pub fn liquidate_borrow_for_perp_pnl( // blocks borrows where oracle is deemed invalid validate!( - !user.is_bankrupt(), + !user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "user bankrupt", )?; @@ -2416,6 +2461,12 @@ pub fn liquidate_borrow_for_perp_pnl( "Perp position must have position pnl" )?; + validate!( + !user_position.is_isolated(), + ErrorCode::InvalidPerpPositionToLiquidate, + "Perp position is an isolated position" + )?; + let market = perp_market_map.get_ref(&perp_market_index)?; let quote_spot_market = spot_market_map.get_ref(&market.quote_spot_market_index)?; @@ -2494,15 +2545,19 @@ pub fn liquidate_borrow_for_perp_pnl( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { + if !user.is_cross_margin_being_liquidated() + && margin_calculation.meets_cross_margin_requirement() + { msg!("margin calculation {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_being_liquidated() && margin_calculation.can_exit_liquidation()? { - user.exit_liquidation(); + } else if user.is_cross_margin_being_liquidated() + && margin_calculation.can_exit_cross_margin_liquidation()? + { + user.exit_cross_margin_liquidation(); return Ok(()); } - let liquidation_id = user.enter_liquidation(slot)?; + let liquidation_id = user.enter_cross_margin_liquidation(slot)?; let mut margin_freed = 0_u64; let canceled_order_ids = orders::cancel_orders( @@ -2518,6 +2573,7 @@ pub fn liquidate_borrow_for_perp_pnl( None, None, None, + true, )?; // check if user exited liquidation territory @@ -2531,15 +2587,15 @@ pub fn liquidate_borrow_for_perp_pnl( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - let initial_margin_shortage = margin_calculation.margin_shortage()?; - let new_margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let initial_margin_shortage = margin_calculation.cross_margin_margin_shortage()?; + let new_margin_shortage = intermediate_margin_calculation.cross_margin_margin_shortage()?; margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) .cast::()?; user.increment_margin_freed(margin_freed)?; - if intermediate_margin_calculation.can_exit_liquidation()? { + if intermediate_margin_calculation.can_exit_cross_margin_liquidation()? { let market = perp_market_map.get_ref(&perp_market_index)?; let market_oracle_price = oracle_map.get_price_data(&market.oracle_id())?.price; @@ -2551,7 +2607,7 @@ pub fn liquidate_borrow_for_perp_pnl( liquidator: *liquidator_key, margin_requirement: margin_calculation.margin_requirement, total_collateral: margin_calculation.total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), canceled_order_ids, margin_freed, liquidate_borrow_for_perp_pnl: LiquidateBorrowForPerpPnlRecord { @@ -2565,16 +2621,16 @@ pub fn liquidate_borrow_for_perp_pnl( ..LiquidationRecord::default() }); - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); return Ok(()); } intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; - let margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let margin_shortage = intermediate_margin_calculation.cross_margin_margin_shortage()?; let liability_weight_with_buffer = liability_weight.safe_add(liquidation_margin_buffer_ratio)?; @@ -2711,14 +2767,15 @@ pub fn liquidate_borrow_for_perp_pnl( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + None, )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; user.increment_margin_freed(margin_freed_from_liability)?; if liability_transfer >= liability_transfer_to_cover_margin_shortage { - user.exit_liquidation(); - } else if is_user_bankrupt(user) { - user.enter_bankruptcy(); + user.exit_cross_margin_liquidation(); + } else if is_cross_margin_bankrupt(user) { + user.enter_cross_margin_bankruptcy(); } let liquidator_meets_initial_margin_requirement = @@ -2743,7 +2800,7 @@ pub fn liquidate_borrow_for_perp_pnl( liquidator: *liquidator_key, margin_requirement: margin_calculation.margin_requirement, total_collateral: margin_calculation.total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), margin_freed, liquidate_borrow_for_perp_pnl: LiquidateBorrowForPerpPnlRecord { perp_market_index, @@ -2782,8 +2839,10 @@ pub fn liquidate_perp_pnl_for_deposit( // blocked when 1) user deposit oracle is deemed invalid // or 2) user has outstanding liability with higher tier + let liquidation_mode = get_perp_liquidation_mode(&user, perp_market_index)?; + validate!( - !user.is_bankrupt(), + !liquidation_mode.is_user_bankrupt(&user)?, ErrorCode::UserBankrupt, "user bankrupt", )?; @@ -2831,13 +2890,7 @@ pub fn liquidate_perp_pnl_for_deposit( e })?; - user.get_spot_position(asset_market_index).map_err(|_| { - msg!( - "User does not have a spot balance for asset market {}", - asset_market_index - ); - ErrorCode::CouldNotFindSpotPosition - })?; + liquidation_mode.validate_spot_position(user, asset_market_index)?; liquidator .force_get_perp_position_mut(perp_market_index) @@ -2888,22 +2941,8 @@ pub fn liquidate_perp_pnl_for_deposit( )?; let token_price = asset_price_data.price; - let spot_position = user.get_spot_position(asset_market_index)?; - - validate!( - spot_position.balance_type == SpotBalanceType::Deposit, - ErrorCode::WrongSpotBalanceType, - "User did not have a deposit for the asset market" - )?; - - let token_amount = spot_position.get_token_amount(&asset_market)?; - validate!( - token_amount != 0, - ErrorCode::InvalidSpotPosition, - "asset token amount zero for market index = {}", - asset_market_index - )?; + let token_amount = liquidation_mode.get_spot_token_amount(user, &asset_market)?; ( token_amount, @@ -2973,17 +3012,24 @@ pub fn liquidate_perp_pnl_for_deposit( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { + let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(&user)?; + if !user_is_being_liquidated + && liquidation_mode.meets_margin_requirements(&margin_calculation)? + { msg!("margin calculation {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_being_liquidated() && margin_calculation.can_exit_liquidation()? { - user.exit_liquidation(); + } else if user_is_being_liquidated + && liquidation_mode.can_exit_liquidation(&margin_calculation)? + { + liquidation_mode.exit_liquidation(user)?; return Ok(()); } - let liquidation_id = user.enter_liquidation(slot)?; + let liquidation_id = liquidation_mode.enter_liquidation(user, slot)?; let mut margin_freed = 0_u64; + let (cancel_orders_market_type, cancel_orders_market_index) = + liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( user, user_key, @@ -2994,13 +3040,14 @@ pub fn liquidate_perp_pnl_for_deposit( now, slot, OrderActionExplanation::Liquidation, + cancel_orders_market_type, + cancel_orders_market_index, None, - None, - None, + true, )?; - let (safest_tier_spot_liability, safest_tier_perp_liability) = - calculate_user_safest_position_tiers(user, perp_market_map, spot_market_map)?; + let (safest_tier_spot_liability, safest_tier_perp_liability) = liquidation_mode + .calculate_user_safest_position_tiers(user, perp_market_map, spot_market_map)?; let is_contract_tier_violation = !(contract_tier.is_as_safe_as(&safest_tier_perp_liability, &safest_tier_spot_liability)); @@ -3015,29 +3062,33 @@ pub fn liquidate_perp_pnl_for_deposit( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - let initial_margin_shortage = margin_calculation.margin_shortage()?; - let new_margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let initial_margin_shortage = liquidation_mode.margin_shortage(&margin_calculation)?; + let new_margin_shortage = + liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) .cast::()?; - user.increment_margin_freed(margin_freed)?; + liquidation_mode.increment_free_margin(user, margin_freed)?; - let exiting_liq_territory = intermediate_margin_calculation.can_exit_liquidation()?; + let exiting_liq_territory = + liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)?; if exiting_liq_territory || is_contract_tier_violation { let market = perp_market_map.get_ref(&perp_market_index)?; let market_oracle_price = oracle_map.get_price_data(&market.oracle_id())?.price; + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, liquidation_type: LiquidationType::LiquidatePerpPnlForDeposit, user: *user_key, liquidator: *liquidator_key, - margin_requirement: margin_calculation.margin_requirement, - total_collateral: margin_calculation.total_collateral, - bankrupt: user.is_bankrupt(), + margin_requirement, + total_collateral, + bankrupt: liquidation_mode.is_user_bankrupt(&user)?, canceled_order_ids, margin_freed, liquidate_perp_pnl_for_deposit: LiquidatePerpPnlForDepositRecord { @@ -3048,11 +3099,12 @@ pub fn liquidate_perp_pnl_for_deposit( asset_price, asset_transfer: 0, }, + bit_flags, ..LiquidationRecord::default() }); if exiting_liq_territory { - user.exit_liquidation(); + liquidation_mode.exit_liquidation(user)?; } else if is_contract_tier_violation { msg!( "return early after cancel orders: liquidating contract tier={:?} pnl is riskier than outstanding {:?} & {:?}", @@ -3067,7 +3119,7 @@ pub fn liquidate_perp_pnl_for_deposit( intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; if is_contract_tier_violation { @@ -3080,7 +3132,7 @@ pub fn liquidate_perp_pnl_for_deposit( return Err(ErrorCode::TierViolationLiquidatingPerpPnl); } - let margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let margin_shortage = liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; let pnl_liability_weight_plus_buffer = pnl_liability_weight.safe_add(liquidation_margin_buffer_ratio)?; @@ -3098,7 +3150,7 @@ pub fn liquidate_perp_pnl_for_deposit( 0, // no if fee )?; - let max_pct_allowed = calculate_max_pct_to_liquidate( + let max_pct_allowed = liquidation_mode.calculate_max_pct_to_liquidate( user, margin_shortage, slot, @@ -3186,12 +3238,10 @@ pub fn liquidate_perp_pnl_for_deposit( Some(asset_transfer), )?; - update_spot_balances_and_cumulative_deposits( + liquidation_mode.decrease_spot_token_amount( + user, asset_transfer, - &SpotBalanceType::Borrow, &mut asset_market, - user.get_spot_position_mut(asset_market_index)?, - false, Some(asset_transfer), )?; } @@ -3212,14 +3262,15 @@ pub fn liquidate_perp_pnl_for_deposit( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + Some(liquidation_mode.as_ref()), )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; - user.increment_margin_freed(margin_freed_from_liability)?; + liquidation_mode.increment_free_margin(user, margin_freed_from_liability)?; if pnl_transfer >= pnl_transfer_to_cover_margin_shortage { - user.exit_liquidation(); - } else if is_user_bankrupt(user) { - user.enter_bankruptcy(); + liquidation_mode.exit_liquidation(user)?; + } else if liquidation_mode.should_user_enter_bankruptcy(user)? { + liquidation_mode.enter_bankruptcy(user)?; } let liquidator_meets_initial_margin_requirement = @@ -3236,15 +3287,17 @@ pub fn liquidate_perp_pnl_for_deposit( oracle_map.get_price_data(&market.oracle_id())?.price }; + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, liquidation_type: LiquidationType::LiquidatePerpPnlForDeposit, user: *user_key, liquidator: *liquidator_key, - margin_requirement: margin_calculation.margin_requirement, - total_collateral: margin_calculation.total_collateral, - bankrupt: user.is_bankrupt(), + margin_requirement, + total_collateral, + bankrupt: liquidation_mode.is_user_bankrupt(&user)?, margin_freed, liquidate_perp_pnl_for_deposit: LiquidatePerpPnlForDepositRecord { perp_market_index, @@ -3254,6 +3307,7 @@ pub fn liquidate_perp_pnl_for_deposit( asset_price, asset_transfer, }, + bit_flags, ..LiquidationRecord::default() }); @@ -3272,28 +3326,32 @@ pub fn resolve_perp_bankruptcy( now: i64, insurance_fund_vault_balance: u64, ) -> DriftResult { - if !user.is_bankrupt() && is_user_bankrupt(user) { - user.enter_bankruptcy(); + let liquidation_mode = get_perp_liquidation_mode(&user, market_index)?; + + if !liquidation_mode.is_user_bankrupt(&user)? + && liquidation_mode.should_user_enter_bankruptcy(&user)? + { + liquidation_mode.enter_bankruptcy(user)?; } validate!( - user.is_bankrupt(), + liquidation_mode.is_user_bankrupt(&user)?, ErrorCode::UserNotBankrupt, "user not bankrupt", )?; - validate!( - !liquidator.is_being_liquidated(), - ErrorCode::UserIsBeingLiquidated, - "liquidator being liquidated", - )?; - validate!( !liquidator.is_bankrupt(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; + validate!( + !liquidator.is_being_liquidated(), + ErrorCode::UserIsBeingLiquidated, + "liquidator being liquidated", + )?; + let market = perp_market_map.get_ref(&market_index)?; validate!( @@ -3324,11 +3382,7 @@ pub fn resolve_perp_bankruptcy( "user must have negative pnl" )?; - let MarginCalculation { - margin_requirement, - total_collateral, - .. - } = calculate_margin_requirement_and_total_collateral_and_liability_info( + let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, @@ -3456,12 +3510,14 @@ pub fn resolve_perp_bankruptcy( } // exit bankruptcy - if !is_user_bankrupt(user) { - user.exit_bankruptcy(); + if !liquidation_mode.should_user_enter_bankruptcy(user)? { + liquidation_mode.exit_bankruptcy(user)?; } let liquidation_id = user.next_liquidation_id.safe_sub(1)?; + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -3479,6 +3535,7 @@ pub fn resolve_perp_bankruptcy( clawback_user_payment: None, cumulative_funding_rate_delta, }, + bit_flags, ..LiquidationRecord::default() }); @@ -3497,28 +3554,28 @@ pub fn resolve_spot_bankruptcy( now: i64, insurance_fund_vault_balance: u64, ) -> DriftResult { - if !user.is_bankrupt() && is_user_bankrupt(user) { - user.enter_bankruptcy(); + if !user.is_cross_margin_bankrupt() && is_cross_margin_bankrupt(user) { + user.enter_cross_margin_bankruptcy(); } validate!( - user.is_bankrupt(), + user.is_cross_margin_bankrupt(), ErrorCode::UserNotBankrupt, "user not bankrupt", )?; - validate!( - !liquidator.is_being_liquidated(), - ErrorCode::UserIsBeingLiquidated, - "liquidator being liquidated", - )?; - validate!( !liquidator.is_bankrupt(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; + validate!( + !liquidator.is_being_liquidated(), + ErrorCode::UserIsBeingLiquidated, + "liquidator being liquidated", + )?; + let market = spot_market_map.get_ref(&market_index)?; validate!( @@ -3612,8 +3669,8 @@ pub fn resolve_spot_bankruptcy( } // exit bankruptcy - if !is_user_bankrupt(user) { - user.exit_bankruptcy(); + if !is_cross_margin_bankrupt(user) { + user.exit_cross_margin_bankruptcy(); } let liquidation_id = user.next_liquidation_id.safe_sub(1)?; @@ -3646,6 +3703,7 @@ pub fn calculate_margin_freed( oracle_map: &mut OracleMap, liquidation_margin_buffer_ratio: u32, initial_margin_shortage: u128, + liquidation_mode: Option<&dyn LiquidatePerpMode>, ) -> DriftResult<(u64, MarginCalculation)> { let margin_calculation_after = calculate_margin_requirement_and_total_collateral_and_liability_info( @@ -3656,7 +3714,11 @@ pub fn calculate_margin_freed( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - let new_margin_shortage = margin_calculation_after.margin_shortage()?; + let new_margin_shortage = if let Some(liquidation_mode) = liquidation_mode { + liquidation_mode.margin_shortage(&margin_calculation_after)? + } else { + margin_calculation_after.cross_margin_margin_shortage()? + }; let margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) @@ -3694,11 +3756,28 @@ pub fn set_user_status_to_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { - msg!("margin calculation: {:?}", margin_calculation); + let mut updated_liquidation_status = false; + if !user.is_cross_margin_being_liquidated() + && !margin_calculation.meets_cross_margin_requirement() + { + updated_liquidation_status = true; + user.enter_cross_margin_liquidation(slot)?; + } + + for (market_index, isolated_margin_calculation) in + margin_calculation.isolated_margin_calculations.iter() + { + if !user.is_isolated_margin_being_liquidated(*market_index)? + && !isolated_margin_calculation.meets_margin_requirement() + { + updated_liquidation_status = true; + user.enter_isolated_margin_liquidation(*market_index, slot)?; + } + } + + if !updated_liquidation_status { return Err(ErrorCode::SufficientCollateral); - } else { - user.enter_liquidation(slot)?; } + Ok(()) } diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index e8cf21acde..aab1707765 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -1,6 +1,7 @@ pub mod liquidate_perp { use crate::math::constants::ONE_HOUR; use crate::state::state::State; + use std::collections::BTreeSet; use std::str::FromStr; use anchor_lang::Owner; @@ -17,7 +18,7 @@ pub mod liquidate_perp { QUOTE_PRECISION, QUOTE_PRECISION_I128, QUOTE_PRECISION_I64, SPOT_BALANCE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, }; - use crate::math::liquidation::is_user_being_liquidated; + use crate::math::liquidation::is_cross_margin_being_liquidated; use crate::math::margin::{ calculate_margin_requirement_and_total_collateral_and_liability_info, MarginRequirementType, }; @@ -30,7 +31,8 @@ pub mod liquidate_perp { use crate::state::spot_market::{SpotBalanceType, SpotMarket}; use crate::state::spot_market_map::SpotMarketMap; use crate::state::user::{ - MarginMode, Order, OrderStatus, OrderType, PerpPosition, SpotPosition, User, UserStats, + MarginMode, Order, OrderStatus, OrderType, PerpPosition, PositionFlag, SpotPosition, User, + UserStats, }; use crate::test_utils::*; use crate::test_utils::{get_orders, get_positions, get_pyth_price, get_spot_positions}; @@ -904,7 +906,7 @@ pub mod liquidate_perp { ) .unwrap(); assert_eq!(margin_req, 140014010000); - assert!(!is_user_being_liquidated( + assert!(!is_cross_margin_being_liquidated( &user, &perp_market_map, &spot_market_map, @@ -930,7 +932,7 @@ pub mod liquidate_perp { ) .unwrap(); assert_eq!(margin_req2, 1040104010000); - assert!(is_user_being_liquidated( + assert!(is_cross_margin_being_liquidated( &user, &perp_market_map, &spot_market_map, @@ -2197,7 +2199,7 @@ pub mod liquidate_perp { .unwrap(); let market_after = perp_market_map.get_ref(&0).unwrap(); - assert!(!user.is_being_liquidated()); + assert!(!user.is_cross_margin_being_liquidated()); assert_eq!(market_after.amm.total_liquidation_fee, 41787043); } @@ -2351,7 +2353,7 @@ pub mod liquidate_perp { .unwrap(); // user out of liq territory - assert!(!user.is_being_liquidated()); + assert!(!user.is_cross_margin_being_liquidated()); let oracle_price = oracle_map .get_price_data(&(oracle_price_key, OracleSource::Pyth)) @@ -2375,6 +2377,196 @@ pub mod liquidate_perp { let market_after = perp_market_map.get_ref(&0).unwrap(); assert_eq!(market_after.amm.total_liquidation_fee, 750000) } + + #[test] + pub fn unhealthy_cross_margin_doesnt_cause_isolated_position_liquidation() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let mut market2 = PerpMarket { + market_index: 1, + 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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market2, PerpMarket, market2_account_info); + + let market_account_infos = vec![market_account_info, market2_account_info]; + let market_set = BTreeSet::default(); + let perp_market_map = + PerpMarketMap::load(&market_set, &mut market_account_infos.iter().peekable()).unwrap(); + + let mut 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, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + let mut perp_positions = [PerpPosition::default(); 8]; + perp_positions[0] = PerpPosition { + market_index: 0, + base_asset_amount: BASE_PRECISION_I64, + quote_asset_amount: -150 * QUOTE_PRECISION_I64, + quote_entry_amount: -150 * QUOTE_PRECISION_I64, + quote_break_even_amount: -150 * QUOTE_PRECISION_I64, + ..PerpPosition::default() + }; + perp_positions[1] = PerpPosition { + market_index: 1, + base_asset_amount: BASE_PRECISION_I64, + quote_asset_amount: -50 * QUOTE_PRECISION_I64, + quote_entry_amount: -50 * QUOTE_PRECISION_I64, + quote_break_even_amount: -150 * QUOTE_PRECISION_I64, + isolated_position_scaled_balance: 200 * SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }; + let mut user = User { + perp_positions, + spot_positions, + ..User::default() + }; + + let mut liquidator = User { + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let liquidator_key = Pubkey::default(); + + let mut user_stats = UserStats::default(); + let mut liquidator_stats = UserStats::default(); + let state = State { + liquidation_margin_buffer_ratio: 10, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + + let isolated_position_before = user.perp_positions[1].clone(); + + let result = liquidate_perp( + 1, + BASE_PRECISION_U64, + None, + &mut user, + &user_key, + &mut user_stats, + &mut liquidator, + &liquidator_key, + &mut liquidator_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + &state, + ); + + assert_eq!(result, Err(ErrorCode::SufficientCollateral)); + + liquidate_perp( + 0, + BASE_PRECISION_U64, + None, + &mut user, + &user_key, + &mut user_stats, + &mut liquidator, + &liquidator_key, + &mut liquidator_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + &state, + ) + .unwrap(); + + let isolated_position_after = user.perp_positions[1].clone(); + + assert_eq!(isolated_position_before, isolated_position_after); + } } pub mod liquidate_perp_with_fill { @@ -4256,7 +4448,7 @@ pub mod liquidate_spot { .unwrap(); assert_eq!(user.last_active_slot, 1); - assert_eq!(user.is_being_liquidated(), true); + assert_eq!(user.is_cross_margin_being_liquidated(), true); assert_eq!(user.liquidation_margin_freed, 7000031); assert_eq!(user.spot_positions[0].scaled_balance, 990558159000); assert_eq!(user.spot_positions[1].scaled_balance, 9406768999); @@ -4326,7 +4518,7 @@ pub mod liquidate_spot { let pct_margin_freed = (user.liquidation_margin_freed as u128) * PRICE_PRECISION / (margin_shortage + user.liquidation_margin_freed as u128); assert_eq!(pct_margin_freed, 433267); // ~43.3% - assert_eq!(user.is_being_liquidated(), true); + assert_eq!(user.is_cross_margin_being_liquidated(), true); let slot = 136_u64; liquidate_spot( @@ -4353,7 +4545,7 @@ pub mod liquidate_spot { assert_eq!(user.liquidation_margin_freed, 0); assert_eq!(user.spot_positions[0].scaled_balance, 455580082000); assert_eq!(user.spot_positions[1].scaled_balance, 4067681997); - assert_eq!(user.is_being_liquidated(), false); + assert_eq!(user.is_cross_margin_being_liquidated(), false); } #[test] @@ -6873,7 +7065,7 @@ pub mod liquidate_perp_pnl_for_deposit { ) .unwrap(); - let margin_shortage = calc.margin_shortage().unwrap(); + let margin_shortage = calc.cross_margin_margin_shortage().unwrap(); let pct_margin_freed = (user.liquidation_margin_freed as u128) * PRICE_PRECISION / (margin_shortage + user.liquidation_margin_freed as u128); @@ -6914,7 +7106,7 @@ pub mod liquidate_perp_pnl_for_deposit { ) .unwrap(); - let margin_shortage = calc.margin_shortage().unwrap(); + let margin_shortage = calc.cross_margin_margin_shortage().unwrap(); let pct_margin_freed = (user.liquidation_margin_freed as u128) * PRICE_PRECISION / (margin_shortage + user.liquidation_margin_freed as u128); @@ -8560,7 +8752,7 @@ pub mod liquidate_spot_with_swap { ) .unwrap(); - assert_eq!(user.is_being_liquidated(), false); + assert_eq!(user.is_cross_margin_being_liquidated(), false); let quote_spot_market = spot_market_map.get_ref(&0).unwrap(); let sol_spot_market = spot_market_map.get_ref(&1).unwrap(); @@ -8910,3 +9102,1768 @@ mod liquidate_dust_spot_market { assert_eq!(result, Ok(())); } } + +pub mod liquidate_isolated_perp { + use crate::math::constants::ONE_HOUR; + use crate::state::state::State; + use std::collections::BTreeSet; + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::controller::liquidation::{liquidate_perp, liquidate_spot}; + use crate::controller::position::PositionDirection; + use crate::create_anchor_account_info; + use crate::error::ErrorCode; + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I128, BASE_PRECISION_I64, BASE_PRECISION_U64, + LIQUIDATION_FEE_PRECISION, LIQUIDATION_PCT_PRECISION, MARGIN_PRECISION, + MARGIN_PRECISION_U128, PEG_PRECISION, PRICE_PRECISION, PRICE_PRECISION_U64, + QUOTE_PRECISION, QUOTE_PRECISION_I128, QUOTE_PRECISION_I64, SPOT_BALANCE_PRECISION_U64, + SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, + }; + use crate::math::liquidation::is_cross_margin_being_liquidated; + use crate::math::margin::{ + calculate_margin_requirement_and_total_collateral_and_liability_info, MarginRequirementType, + }; + use crate::math::position::calculate_base_asset_value_with_oracle_price; + use crate::state::margin_calculation::{MarginCalculation, MarginContext}; + 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::{ + MarginMode, Order, OrderStatus, OrderType, PerpPosition, PositionFlag, SpotPosition, User, + UserStats, + }; + use crate::test_utils::*; + use crate::test_utils::{get_orders, get_positions, get_pyth_price, get_spot_positions}; + use crate::{create_account_info, PRICE_PRECISION_I64}; + + #[test] + pub fn successful_liquidation_long_perp() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..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 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, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User { + orders: get_orders(Order { + market_index: 0, + status: OrderStatus::Open, + order_type: OrderType::Limit, + direction: PositionDirection::Long, + base_asset_amount: BASE_PRECISION_U64, + slot: 0, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: BASE_PRECISION_I64, + quote_asset_amount: -150 * QUOTE_PRECISION_I64, + quote_entry_amount: -150 * QUOTE_PRECISION_I64, + quote_break_even_amount: -150 * QUOTE_PRECISION_I64, + open_orders: 1, + open_bids: BASE_PRECISION_I64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }), + spot_positions: [SpotPosition::default(); 8], + + ..User::default() + }; + + let mut liquidator = User { + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let liquidator_key = Pubkey::default(); + + let mut user_stats = UserStats::default(); + let mut liquidator_stats = UserStats::default(); + let state = State { + liquidation_margin_buffer_ratio: 10, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + liquidate_perp( + 0, + BASE_PRECISION_U64, + None, + &mut user, + &user_key, + &mut user_stats, + &mut liquidator, + &liquidator_key, + &mut liquidator_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + &state, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].base_asset_amount, 0); + assert_eq!( + user.perp_positions[0].quote_asset_amount, + -51 * QUOTE_PRECISION_I64 + ); + assert_eq!(user.perp_positions[0].open_orders, 0); + assert_eq!(user.perp_positions[0].open_bids, 0); + + assert_eq!( + liquidator.perp_positions[0].base_asset_amount, + BASE_PRECISION_I64 + ); + assert_eq!( + liquidator.perp_positions[0].quote_asset_amount, + -99 * QUOTE_PRECISION_I64 + ); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + assert_eq!(market_after.amm.total_liquidation_fee, 0); + } + + #[test] + pub fn successful_liquidation_short_perp() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: 50 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + funding_period: 3600, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..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 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, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User { + orders: get_orders(Order { + market_index: 0, + status: OrderStatus::Open, + order_type: OrderType::Limit, + direction: PositionDirection::Short, + base_asset_amount: BASE_PRECISION_U64, + slot: 0, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: -BASE_PRECISION_I64, + quote_asset_amount: 50 * QUOTE_PRECISION_I64, + quote_entry_amount: 50 * QUOTE_PRECISION_I64, + quote_break_even_amount: 50 * QUOTE_PRECISION_I64, + open_orders: 1, + open_asks: -BASE_PRECISION_I64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }), + spot_positions: [SpotPosition::default(); 8], + + ..User::default() + }; + + let mut liquidator = User { + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let liquidator_key = Pubkey::default(); + + let mut user_stats = UserStats::default(); + let mut liquidator_stats = UserStats::default(); + let state = State { + liquidation_margin_buffer_ratio: 10, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + liquidate_perp( + 0, + BASE_PRECISION_U64, + None, + &mut user, + &user_key, + &mut user_stats, + &mut liquidator, + &liquidator_key, + &mut liquidator_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + &state, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].base_asset_amount, 0); + assert_eq!( + user.perp_positions[0].quote_asset_amount, + -51 * QUOTE_PRECISION_I64 + ); + assert_eq!(user.perp_positions[0].open_orders, 0); + assert_eq!(user.perp_positions[0].open_bids, 0); + + assert_eq!( + liquidator.perp_positions[0].base_asset_amount, + -BASE_PRECISION_I64 + ); + assert_eq!( + liquidator.perp_positions[0].quote_asset_amount, + 101 * QUOTE_PRECISION_I64 + ); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + assert_eq!(market_after.amm.total_liquidation_fee, 0); + } + + #[test] + pub fn successful_liquidation_to_cover_margin_shortage() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + funding_period: ONE_HOUR, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..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 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, + historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User { + orders: get_orders(Order { + market_index: 0, + status: OrderStatus::Open, + order_type: OrderType::Limit, + direction: PositionDirection::Long, + base_asset_amount: BASE_PRECISION_U64, + slot: 0, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: 2 * BASE_PRECISION_I64, + quote_asset_amount: -200 * QUOTE_PRECISION_I64, + quote_entry_amount: -200 * QUOTE_PRECISION_I64, + quote_break_even_amount: -200 * QUOTE_PRECISION_I64, + open_orders: 1, + open_bids: BASE_PRECISION_I64, + position_flag: PositionFlag::IsolatedPosition as u8, + isolated_position_scaled_balance: 5 * SPOT_BALANCE_PRECISION_U64, + ..PerpPosition::default() + }), + + ..User::default() + }; + + let mut liquidator = User { + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let liquidator_key = Pubkey::default(); + + let mut user_stats = UserStats::default(); + let mut liquidator_stats = UserStats::default(); + let state = State { + liquidation_margin_buffer_ratio: MARGIN_PRECISION / 50, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + liquidate_perp( + 0, + 10 * BASE_PRECISION_U64, + None, + &mut user, + &user_key, + &mut user_stats, + &mut liquidator, + &liquidator_key, + &mut liquidator_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + &state, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].base_asset_amount, 200000000); + assert_eq!(user.perp_positions[0].quote_asset_amount, -23600000); + assert_eq!(user.perp_positions[0].quote_entry_amount, -20000000); + assert_eq!(user.perp_positions[0].quote_break_even_amount, -23600000); + assert_eq!(user.perp_positions[0].open_orders, 0); + assert_eq!(user.perp_positions[0].open_bids, 0); + + let margin_calculation = + calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::liquidation(state.liquidation_margin_buffer_ratio), + ) + .unwrap(); + + let isolated_margin_calculation = margin_calculation + .get_isolated_margin_calculation(0) + .unwrap(); + let total_collateral = isolated_margin_calculation.total_collateral; + let margin_requirement_plus_buffer = + isolated_margin_calculation.margin_requirement_plus_buffer; + + // user out of liq territory + assert_eq!( + total_collateral.unsigned_abs(), + margin_requirement_plus_buffer + ); + + let oracle_price = oracle_map + .get_price_data(&(oracle_price_key, OracleSource::Pyth)) + .unwrap() + .price; + + let perp_value = calculate_base_asset_value_with_oracle_price( + user.perp_positions[0].base_asset_amount as i128, + oracle_price, + ) + .unwrap(); + + let margin_ratio = total_collateral.unsigned_abs() * MARGIN_PRECISION_U128 / perp_value; + + assert_eq!(margin_ratio, 700); + + assert_eq!(liquidator.perp_positions[0].base_asset_amount, 1800000000); + assert_eq!(liquidator.perp_positions[0].quote_asset_amount, -178200000); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + assert_eq!(market_after.amm.total_liquidation_fee, 1800000) + } + + #[test] + pub fn liquidation_over_multiple_slots_takes_one() { + let now = 1_i64; + let slot = 1_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + funding_period: ONE_HOUR, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..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 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, + historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User { + orders: get_orders(Order { + market_index: 0, + status: OrderStatus::Open, + order_type: OrderType::Limit, + direction: PositionDirection::Long, + base_asset_amount: 10 * BASE_PRECISION_U64, + slot: 0, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: 20 * BASE_PRECISION_I64, + quote_asset_amount: -2000 * QUOTE_PRECISION_I64, + quote_entry_amount: -2000 * QUOTE_PRECISION_I64, + quote_break_even_amount: -2000 * QUOTE_PRECISION_I64, + open_orders: 1, + open_bids: 10 * BASE_PRECISION_I64, + position_flag: PositionFlag::IsolatedPosition as u8, + isolated_position_scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..PerpPosition::default() + }), + ..User::default() + }; + + let mut liquidator = User { + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 500 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let liquidator_key = Pubkey::default(); + + let mut user_stats = UserStats::default(); + let mut liquidator_stats = UserStats::default(); + let state = State { + liquidation_margin_buffer_ratio: MARGIN_PRECISION / 50, + initial_pct_to_liquidate: (LIQUIDATION_PCT_PRECISION / 10) as u16, + liquidation_duration: 150, + ..Default::default() + }; + liquidate_perp( + 0, + 20 * BASE_PRECISION_U64, + None, + &mut user, + &user_key, + &mut user_stats, + &mut liquidator, + &liquidator_key, + &mut liquidator_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + &state, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].base_asset_amount, 2000000000); + assert_eq!(user.perp_positions[0].is_being_liquidated(), false); + } + + #[test] + pub fn successful_liquidation_half_of_if_fee() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: 50 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + funding_period: 3600, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + number_of_users: 1, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..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 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, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User { + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: -BASE_PRECISION_I64, + quote_asset_amount: 100 * QUOTE_PRECISION_I64, + quote_entry_amount: 100 * QUOTE_PRECISION_I64, + quote_break_even_amount: 100 * QUOTE_PRECISION_I64, + position_flag: PositionFlag::IsolatedPosition as u8, + isolated_position_scaled_balance: 15 * SPOT_BALANCE_PRECISION_U64 / 10, // $1.5 + ..PerpPosition::default() + }), + ..User::default() + }; + + let mut liquidator = User { + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let liquidator_key = Pubkey::default(); + + let mut user_stats = UserStats::default(); + let mut liquidator_stats = UserStats::default(); + let state = State { + liquidation_margin_buffer_ratio: 10, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + liquidate_perp( + 0, + BASE_PRECISION_U64, + None, + &mut user, + &user_key, + &mut user_stats, + &mut liquidator, + &liquidator_key, + &mut liquidator_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + &state, + ) + .unwrap(); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + // .5% * 100 * .95 =$0.475 + assert_eq!(market_after.amm.total_liquidation_fee, 475000); + } + + #[test] + pub fn successful_liquidation_portion_of_if_fee() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_hardcoded_pyth_price(23244136, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: 50 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + funding_period: 3600, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + number_of_users: 1, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..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 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, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User { + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: -299400000000, + quote_asset_amount: 6959294318, + quote_entry_amount: 6959294318, + quote_break_even_amount: 6959294318, + position_flag: PositionFlag::IsolatedPosition as u8, + isolated_position_scaled_balance: 113838792 * 1000, + ..PerpPosition::default() + }), + ..User::default() + }; + + let mut liquidator = User { + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let liquidator_key = Pubkey::default(); + + let mut user_stats = UserStats::default(); + let mut liquidator_stats = UserStats::default(); + let state = State { + liquidation_margin_buffer_ratio: 200, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + liquidate_perp( + 0, + 300 * BASE_PRECISION_U64, + None, + &mut user, + &user_key, + &mut user_stats, + &mut liquidator, + &liquidator_key, + &mut liquidator_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + &state, + ) + .unwrap(); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + assert!(!user.is_isolated_margin_being_liquidated(0).unwrap()); + assert_eq!(market_after.amm.total_liquidation_fee, 41787043); + } + + #[test] + pub fn unhealthy_isolated_perp_doesnt_cause_cross_margin_liquidation() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + + let mut market2 = market.clone(); + market2.market_index = 1; + create_anchor_account_info!(market2, PerpMarket, market2_account_info); + + let market_account_infos = vec![market_account_info, market2_account_info]; + let market_set = BTreeSet::default(); + let perp_market_map = + PerpMarketMap::load(&market_set, &mut market_account_infos.iter().peekable()).unwrap(); + + let mut spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + initial_liability_weight: SPOT_WEIGHT_PRECISION, + maintenance_liability_weight: SPOT_WEIGHT_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + + let mut spot_market2 = spot_market.clone(); + spot_market2.market_index = 1; + create_anchor_account_info!(spot_market2, SpotMarket, spot_market2_account_info); + + let spot_market_account_infos = vec![spot_market_account_info, spot_market2_account_info]; + let mut spot_market_set = BTreeSet::default(); + spot_market_set.insert(0); + spot_market_set.insert(1); + let spot_market_map = SpotMarketMap::load( + &spot_market_set, + &mut spot_market_account_infos.iter().peekable(), + ) + .unwrap(); + + let mut user = User { + orders: get_orders(Order { + market_index: 0, + status: OrderStatus::Open, + order_type: OrderType::Limit, + direction: PositionDirection::Long, + base_asset_amount: BASE_PRECISION_U64, + slot: 0, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: BASE_PRECISION_I64, + quote_asset_amount: -150 * QUOTE_PRECISION_I64, + quote_entry_amount: -150 * QUOTE_PRECISION_I64, + quote_break_even_amount: -150 * QUOTE_PRECISION_I64, + open_orders: 1, + open_bids: BASE_PRECISION_I64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }), + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + + ..User::default() + }; + + user.spot_positions[1] = SpotPosition { + market_index: 1, + balance_type: SpotBalanceType::Borrow, + scaled_balance: 1 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + + user.perp_positions[1] = PerpPosition { + market_index: 1, + base_asset_amount: BASE_PRECISION_I64, + quote_asset_amount: -100 * QUOTE_PRECISION_I64, + quote_entry_amount: -100 * QUOTE_PRECISION_I64, + quote_break_even_amount: -100 * QUOTE_PRECISION_I64, + ..PerpPosition::default() + }; + + let mut liquidator = User { + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let liquidator_key = Pubkey::default(); + + let mut user_stats = UserStats::default(); + let mut liquidator_stats = UserStats::default(); + let state = State { + liquidation_margin_buffer_ratio: 10, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + let result = liquidate_perp( + 1, + BASE_PRECISION_U64, + None, + &mut user, + &user_key, + &mut user_stats, + &mut liquidator, + &liquidator_key, + &mut liquidator_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + &state, + ); + + assert_eq!(result, Err(ErrorCode::SufficientCollateral)); + + let result = liquidate_spot( + 0, + 1, + 1, + None, + &mut user, + &user_key, + &mut user_stats, + &mut liquidator, + &liquidator_key, + &mut liquidator_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + now, + slot, + &state, + ); + + assert_eq!(result, Err(ErrorCode::SufficientCollateral)); + + let margin_calculation = + calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::liquidation(state.liquidation_margin_buffer_ratio), + ) + .unwrap(); + + assert_eq!(margin_calculation.meets_cross_margin_requirement(), true); + + assert_eq!(margin_calculation.meets_margin_requirement(), false); + + assert_eq!( + margin_calculation + .meets_isolated_margin_requirement(0) + .unwrap(), + false + ); + + let spot_position_one_before = user.spot_positions[0].clone(); + let spot_position_two_before = user.spot_positions[1].clone(); + let perp_position_one_before = user.perp_positions[1].clone(); + liquidate_perp( + 0, + BASE_PRECISION_U64, + None, + &mut user, + &user_key, + &mut user_stats, + &mut liquidator, + &liquidator_key, + &mut liquidator_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + &state, + ) + .unwrap(); + + let spot_position_one_after = user.spot_positions[0].clone(); + let spot_position_two_after = user.spot_positions[1].clone(); + let perp_position_one_after = user.perp_positions[1].clone(); + + assert_eq!(spot_position_one_before, spot_position_one_after); + assert_eq!(spot_position_two_before, spot_position_two_after); + assert_eq!(perp_position_one_before, perp_position_one_after); + } +} + +pub mod liquidate_isolated_perp_pnl_for_deposit { + use crate::state::state::State; + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::controller::liquidation::resolve_perp_bankruptcy; + use crate::controller::liquidation::{liquidate_perp_pnl_for_deposit, liquidate_spot}; + use crate::create_account_info; + use crate::create_anchor_account_info; + use crate::error::ErrorCode; + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I128, LIQUIDATION_FEE_PRECISION, + LIQUIDATION_PCT_PRECISION, MARGIN_PRECISION, PEG_PRECISION, PERCENTAGE_PRECISION, + PRICE_PRECISION, PRICE_PRECISION_U64, QUOTE_PRECISION_I128, QUOTE_PRECISION_I64, + SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, + SPOT_WEIGHT_PRECISION, + }; + use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_liability_info; + use crate::state::margin_calculation::MarginContext; + use crate::state::oracle::HistoricalOracleData; + use crate::state::oracle::OracleSource; + use crate::state::oracle_map::OracleMap; + use crate::state::perp_market::{ContractTier, MarketStatus, PerpMarket, AMM}; + use crate::state::perp_market_map::PerpMarketMap; + use crate::state::spot_market::{AssetTier, SpotBalanceType, SpotMarket}; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::user::{Order, PerpPosition, SpotPosition, User, UserStatus}; + use crate::state::user::{PositionFlag, UserStats}; + use crate::test_utils::*; + use crate::test_utils::{get_positions, get_pyth_price, get_spot_positions}; + + #[test] + pub fn successful_liquidation_liquidator_max_pnl_transfer() { + let now = 0_i64; + 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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: 150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: sol_oracle_price_key, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + unrealized_pnl_initial_asset_weight: 9000, + unrealized_pnl_maintenance_asset_weight: 10000, + number_of_users_with_base: 1, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_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: 200 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: QUOTE_PRECISION_I64, + last_oracle_price_twap_5min: QUOTE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_market, SpotMarket, usdc_spot_market_account_info); + let mut sol_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: 6, + 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, + deposit_balance: SPOT_BALANCE_PRECISION, + borrow_balance: 0, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 1000, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: (sol_oracle_price.agg.price * 99 / 100), + last_oracle_price_twap_5min: (sol_oracle_price.agg.price * 99 / 100), + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_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]; + let mut user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + quote_asset_amount: -100 * QUOTE_PRECISION_I64, + isolated_position_scaled_balance: 90 * SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }), + spot_positions, + ..User::default() + }; + + let mut liquidator = User { + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let liquidator_key = Pubkey::default(); + + liquidate_perp_pnl_for_deposit( + 0, + 0, + 50 * 10_u128.pow(6), // .8 + None, + &mut user, + &user_key, + &mut liquidator, + &liquidator_key, + &market_map, + &spot_market_map, + &mut oracle_map, + now, + slot, + 10, + PERCENTAGE_PRECISION, + 150, + ) + .unwrap(); + + assert_eq!( + user.perp_positions[0].isolated_position_scaled_balance, + 39494950000 + ); + assert_eq!(user.perp_positions[0].quote_asset_amount, -50000000); + + assert_eq!( + liquidator.spot_positions[1].balance_type, + SpotBalanceType::Deposit + ); + assert_eq!(liquidator.spot_positions[0].scaled_balance, 150505050000); + assert_eq!(liquidator.perp_positions[0].quote_asset_amount, -50000000); + } + + #[test] + pub fn successful_liquidation_pnl_transfer_leaves_position_bankrupt() { + let now = 0_i64; + 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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: 150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + base_asset_amount_long: BASE_PRECISION_I128, + oracle: sol_oracle_price_key, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + unrealized_pnl_initial_asset_weight: 9000, + unrealized_pnl_maintenance_asset_weight: 10000, + number_of_users_with_base: 1, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_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: 200 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: QUOTE_PRECISION_I64, + last_oracle_price_twap_5min: QUOTE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_market, SpotMarket, usdc_spot_market_account_info); + let mut sol_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: 6, + 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, + deposit_balance: SPOT_BALANCE_PRECISION, + borrow_balance: 0, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 1000, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: (sol_oracle_price.agg.price * 99 / 100), + last_oracle_price_twap_5min: (sol_oracle_price.agg.price * 99 / 100), + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_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]; + let mut user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + quote_asset_amount: -91 * QUOTE_PRECISION_I64, + isolated_position_scaled_balance: 90 * SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }), + spot_positions, + ..User::default() + }; + + let mut liquidator = User { + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let liquidator_key = Pubkey::default(); + + liquidate_perp_pnl_for_deposit( + 0, + 0, + 200 * 10_u128.pow(6), // .8 + None, + &mut user, + &user_key, + &mut liquidator, + &liquidator_key, + &market_map, + &spot_market_map, + &mut oracle_map, + now, + slot, + MARGIN_PRECISION / 50, + PERCENTAGE_PRECISION, + 150, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 0); + assert_eq!(user.perp_positions[0].quote_asset_amount, -1900000); + assert_eq!( + user.perp_positions[0].position_flag & PositionFlag::Bankrupt as u8, + PositionFlag::Bankrupt as u8 + ); + + assert_eq!(liquidator.spot_positions[0].scaled_balance, 190000000000); + assert_eq!(liquidator.perp_positions[0].quote_asset_amount, -89100000); + + let calc = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::liquidation(MARGIN_PRECISION / 50), + ) + .unwrap(); + + assert_eq!(calc.meets_margin_requirement(), false); + + let market_after = market_map.get_ref(&0).unwrap(); + assert_eq!(market_after.amm.total_liquidation_fee, 0); + drop(market_after); + + resolve_perp_bankruptcy( + 0, + &mut user, + &user_key, + &mut liquidator, + &liquidator_key, + &market_map, + &spot_market_map, + &mut oracle_map, + now, + 0, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 0); + assert_eq!(user.perp_positions[0].quote_asset_amount, 0); + assert_eq!( + user.perp_positions[0].position_flag & PositionFlag::Bankrupt as u8, + 0 + ); + assert_eq!(user.is_being_liquidated(), false); + } +} + +mod liquidation_mode { + use crate::state::liquidation_mode::{ + CrossMarginLiquidatePerpMode, IsolatedMarginLiquidatePerpMode, LiquidatePerpMode, + }; + use std::collections::BTreeSet; + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::create_account_info; + use crate::create_anchor_account_info; + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I128, LIQUIDATION_FEE_PRECISION, MARGIN_PRECISION, + PEG_PRECISION, QUOTE_PRECISION_I128, QUOTE_PRECISION_I64, SPOT_BALANCE_PRECISION, + SPOT_BALANCE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, + }; + use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_liability_info; + use crate::state::margin_calculation::MarginContext; + use crate::state::oracle::HistoricalOracleData; + use crate::state::oracle::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::PositionFlag; + use crate::state::user::{Order, PerpPosition, SpotPosition, User}; + use crate::test_utils::get_pyth_price; + use crate::test_utils::*; + + #[test] + pub fn tests_meets_margin_requirements() { + 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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: 150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: sol_oracle_price_key, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + unrealized_pnl_initial_asset_weight: 9000, + unrealized_pnl_maintenance_asset_weight: 10000, + number_of_users_with_base: 1, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + + let mut market2 = PerpMarket { + market_index: 1, + ..market + }; + create_anchor_account_info!(market2, PerpMarket, market2_account_info); + + let market_account_infos = vec![market_account_info, market2_account_info]; + let market_set = BTreeSet::default(); + let market_map = + PerpMarketMap::load(&market_set, &mut market_account_infos.iter().peekable()).unwrap(); + + let mut usdc_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: 200 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: QUOTE_PRECISION_I64, + last_oracle_price_twap_5min: QUOTE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_market, SpotMarket, usdc_spot_market_account_info); + let mut sol_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: 6, + 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, + deposit_balance: SPOT_BALANCE_PRECISION, + borrow_balance: 0, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 1000, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: (sol_oracle_price.agg.price * 99 / 100), + last_oracle_price_twap_5min: (sol_oracle_price.agg.price * 99 / 100), + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_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: 200 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let mut perp_positions = [PerpPosition::default(); 8]; + perp_positions[0] = PerpPosition { + market_index: 0, + quote_asset_amount: -100 * QUOTE_PRECISION_I64, + isolated_position_scaled_balance: 90 * SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }; + perp_positions[1] = PerpPosition { + market_index: 1, + quote_asset_amount: -100 * QUOTE_PRECISION_I64, + ..PerpPosition::default() + }; + let user_isolated_position_being_liquidated = User { + orders: [Order::default(); 32], + perp_positions, + spot_positions, + ..User::default() + }; + + let isolated_liquidation_mode = IsolatedMarginLiquidatePerpMode::new(0); + let cross_liquidation_mode = CrossMarginLiquidatePerpMode::new(0); + + let liquidation_margin_buffer_ratio = MARGIN_PRECISION / 50; + let margin_calculation = + calculate_margin_requirement_and_total_collateral_and_liability_info( + &user_isolated_position_being_liquidated, + &market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::liquidation(liquidation_margin_buffer_ratio), + ) + .unwrap(); + + assert_eq!( + cross_liquidation_mode + .meets_margin_requirements(&margin_calculation) + .unwrap(), + true + ); + assert_eq!( + isolated_liquidation_mode + .meets_margin_requirements(&margin_calculation) + .unwrap(), + false + ); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 90 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let mut perp_positions = [PerpPosition::default(); 8]; + perp_positions[0] = PerpPosition { + market_index: 0, + quote_asset_amount: -100 * QUOTE_PRECISION_I64, + isolated_position_scaled_balance: 200 * SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }; + perp_positions[1] = PerpPosition { + market_index: 1, + quote_asset_amount: -100 * QUOTE_PRECISION_I64, + ..PerpPosition::default() + }; + let user_cross_margin_being_liquidated = User { + orders: [Order::default(); 32], + perp_positions, + spot_positions, + ..User::default() + }; + + let margin_calculation = + calculate_margin_requirement_and_total_collateral_and_liability_info( + &user_cross_margin_being_liquidated, + &market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::liquidation(liquidation_margin_buffer_ratio), + ) + .unwrap(); + + assert_eq!( + cross_liquidation_mode + .meets_margin_requirements(&margin_calculation) + .unwrap(), + false + ); + assert_eq!( + isolated_liquidation_mode + .meets_margin_requirements(&margin_calculation) + .unwrap(), + true + ); + } +} diff --git a/programs/drift/src/controller/mod.rs b/programs/drift/src/controller/mod.rs index 5ebdb9772a..db15dfddf5 100644 --- a/programs/drift/src/controller/mod.rs +++ b/programs/drift/src/controller/mod.rs @@ -1,6 +1,7 @@ pub mod amm; pub mod funding; pub mod insurance; +pub mod isolated_position; pub mod liquidation; pub mod orders; pub mod pda; diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 6d4533dd33..05ee36566e 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -541,8 +541,15 @@ pub fn cancel_orders( market_type: Option, market_index: Option, direction: Option, + skip_isolated_positions: bool, ) -> DriftResult> { let mut canceled_order_ids: Vec = vec![]; + let isolated_position_market_indexes = user + .perp_positions + .iter() + .filter(|position| position.is_isolated()) + .map(|position| position.market_index) + .collect::>(); for order_index in 0..user.orders.len() { if user.orders[order_index].status != OrderStatus::Open { continue; @@ -556,6 +563,10 @@ pub fn cancel_orders( if user.orders[order_index].market_index != market_index { continue; } + } else if skip_isolated_positions + && isolated_position_market_indexes.contains(&user.orders[order_index].market_index) + { + continue; } if let Some(direction) = direction { @@ -1510,7 +1521,7 @@ fn get_maker_orders_info( let mut maker = load_mut!(user_account_loader)?; - if maker.is_being_liquidated() || maker.is_bankrupt() { + if maker.is_being_liquidated() { continue; } @@ -2012,16 +2023,31 @@ fn fulfill_perp_order( )?; if !taker_margin_calculation.meets_margin_requirement() { + let (margin_requirement, total_collateral) = + if taker_margin_calculation.has_isolated_margin_calculation(market_index) { + let isolated_margin_calculation = + taker_margin_calculation.get_isolated_margin_calculation(market_index)?; + ( + isolated_margin_calculation.margin_requirement, + isolated_margin_calculation.total_collateral, + ) + } else { + ( + taker_margin_calculation.margin_requirement, + taker_margin_calculation.total_collateral, + ) + }; + msg!( "taker breached fill requirements (margin requirement {}) (total_collateral {})", - taker_margin_calculation.margin_requirement, - taker_margin_calculation.total_collateral + margin_requirement, + total_collateral ); return Err(ErrorCode::InsufficientCollateral); } } - for (maker_key, maker_base_asset_amount_filled) in maker_fills { + for (maker_key, (maker_base_asset_amount_filled)) in maker_fills { let mut maker = makers_and_referrer.get_ref_mut(&maker_key)?; let maker_stats = if maker.authority == user.authority { @@ -2072,11 +2098,26 @@ fn fulfill_perp_order( } if !maker_margin_calculation.meets_margin_requirement() { + let (margin_requirement, total_collateral) = + if maker_margin_calculation.has_isolated_margin_calculation(market_index) { + let isolated_margin_calculation = + maker_margin_calculation.get_isolated_margin_calculation(market_index)?; + ( + isolated_margin_calculation.margin_requirement, + isolated_margin_calculation.total_collateral, + ) + } else { + ( + maker_margin_calculation.margin_requirement, + maker_margin_calculation.total_collateral, + ) + }; + msg!( "maker ({}) breached fill requirements (margin requirement {}) (total_collateral {})", maker_key, - maker_margin_calculation.margin_requirement, - maker_margin_calculation.total_collateral + margin_requirement, + total_collateral ); return Err(ErrorCode::InsufficientCollateral); } @@ -3376,6 +3417,9 @@ pub fn force_cancel_orders( ErrorCode::SufficientCollateral )?; + let cross_margin_meets_initial_margin_requirement = + margin_calc.meets_cross_margin_requirement(); + let mut total_fee = 0_u64; for order_index in 0..user.orders.len() { @@ -3402,6 +3446,10 @@ pub fn force_cancel_orders( continue; } + if cross_margin_meets_initial_margin_requirement { + continue; + } + state.spot_fee_structure.flat_filler_fee } MarketType::Perp => { @@ -3416,6 +3464,18 @@ pub fn force_cancel_orders( continue; } + if !user.get_perp_position(market_index)?.is_isolated() { + if cross_margin_meets_initial_margin_requirement { + continue; + } + } else { + let meets_isolated_margin_requirement = + margin_calc.meets_isolated_margin_requirement(market_index)?; + if meets_isolated_margin_requirement { + continue; + } + } + state.perp_fee_structure.flat_filler_fee } }; @@ -4224,7 +4284,7 @@ fn get_spot_maker_orders_info( let mut maker = load_mut!(user_account_loader)?; - if maker.is_being_liquidated() || maker.is_bankrupt() { + if maker.is_being_liquidated() { continue; } diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 11911e0356..d6db84ab8d 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -242,10 +242,21 @@ pub fn settle_pnl( let user_unsettled_pnl: i128 = user.perp_positions[position_index].get_claimable_pnl(oracle_price, max_pnl_pool_excess)?; + let is_isolated_position = user.perp_positions[position_index].is_isolated(); + + let user_quote_token_amount = if is_isolated_position { + user.perp_positions[position_index] + .get_isolated_token_amount(spot_market)? + .cast()? + } else { + user.get_quote_spot_position() + .get_signed_token_amount(spot_market)? + }; + let pnl_to_settle_with_user = update_pool_balances( perp_market, spot_market, - user.get_quote_spot_position(), + user_quote_token_amount, user_unsettled_pnl, now, )?; @@ -287,17 +298,43 @@ pub fn settle_pnl( ); } - update_spot_balances( - pnl_to_settle_with_user.unsigned_abs(), - if pnl_to_settle_with_user > 0 { - &SpotBalanceType::Deposit - } else { - &SpotBalanceType::Borrow - }, - spot_market, - user.get_quote_spot_position_mut(), - false, - )?; + if is_isolated_position { + let perp_position = &mut user.perp_positions[position_index]; + if pnl_to_settle_with_user < 0 { + let token_amount = perp_position.get_isolated_token_amount(spot_market)?; + + validate!( + token_amount >= pnl_to_settle_with_user.unsigned_abs(), + ErrorCode::InsufficientCollateralForSettlingPNL, + "user has insufficient deposit for market {}", + market_index + )?; + } + + update_spot_balances( + pnl_to_settle_with_user.unsigned_abs(), + if pnl_to_settle_with_user > 0 { + &SpotBalanceType::Deposit + } else { + &SpotBalanceType::Borrow + }, + spot_market, + perp_position, + false, + )?; + } else { + update_spot_balances( + pnl_to_settle_with_user.unsigned_abs(), + if pnl_to_settle_with_user > 0 { + &SpotBalanceType::Deposit + } else { + &SpotBalanceType::Borrow + }, + spot_market, + user.get_quote_spot_position_mut(), + false, + )?; + } update_quote_asset_amount( &mut user.perp_positions[position_index], @@ -400,6 +437,7 @@ pub fn settle_expired_position( Some(MarketType::Perp), Some(perp_market_index), None, + true, )?; let quote_spot_market = &mut spot_market_map.get_quote_spot_market_mut()?; diff --git a/programs/drift/src/controller/pnl/delisting.rs b/programs/drift/src/controller/pnl/delisting.rs index eafbd2148b..70f81a716b 100644 --- a/programs/drift/src/controller/pnl/delisting.rs +++ b/programs/drift/src/controller/pnl/delisting.rs @@ -2336,7 +2336,7 @@ pub mod delisting_test { let oracle_price_data = oracle_map.get_price_data(&market.oracle_id()).unwrap(); let strict_quote_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (perp_margin_requirement, weighted_pnl, _, _, _) = + let (perp_margin_requirement, weighted_pnl, _, _) = calculate_perp_position_value_and_pnl( &shorter.perp_positions[0], &market, @@ -2345,7 +2345,6 @@ pub mod delisting_test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -2383,8 +2382,8 @@ pub mod delisting_test { let mut shorter_user_stats = UserStats::default(); let mut liq_user_stats = UserStats::default(); - assert_eq!(shorter.is_being_liquidated(), false); - assert_eq!(shorter.is_bankrupt(), false); + assert_eq!(shorter.is_cross_margin_being_liquidated(), false); + assert_eq!(shorter.is_cross_margin_bankrupt(), false); let state = State { liquidation_margin_buffer_ratio: 10, ..Default::default() @@ -2408,15 +2407,15 @@ pub mod delisting_test { ) .unwrap(); - assert_eq!(shorter.is_being_liquidated(), true); - assert_eq!(shorter.is_bankrupt(), false); + assert_eq!(shorter.is_cross_margin_being_liquidated(), true); + assert_eq!(shorter.is_cross_margin_bankrupt(), false); { let market = market_map.get_ref_mut(&0).unwrap(); let oracle_price_data = oracle_map.get_price_data(&market.oracle_id()).unwrap(); let strict_quote_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (perp_margin_requirement, weighted_pnl, _, _, _) = + let (perp_margin_requirement, weighted_pnl, _, _) = calculate_perp_position_value_and_pnl( &shorter.perp_positions[0], &market, @@ -2425,7 +2424,6 @@ pub mod delisting_test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -2491,8 +2489,8 @@ pub mod delisting_test { ) .unwrap(); - assert_eq!(shorter.is_being_liquidated(), true); - assert_eq!(shorter.is_bankrupt(), false); + assert_eq!(shorter.is_cross_margin_being_liquidated(), true); + assert_eq!(shorter.is_cross_margin_bankrupt(), false); { let mut market = market_map.get_ref_mut(&0).unwrap(); @@ -2504,7 +2502,7 @@ pub mod delisting_test { assert_eq!(market.amm.cumulative_funding_rate_short, 0); let strict_quote_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (perp_margin_requirement, weighted_pnl, _, _, _) = + let (perp_margin_requirement, weighted_pnl, _, _) = calculate_perp_position_value_and_pnl( &shorter.perp_positions[0], &market, @@ -2513,7 +2511,6 @@ pub mod delisting_test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -2583,8 +2580,8 @@ pub mod delisting_test { ) .unwrap(); - assert_eq!(shorter.is_being_liquidated(), true); - assert_eq!(shorter.is_bankrupt(), true); + assert_eq!(shorter.is_cross_margin_being_liquidated(), true); + assert_eq!(shorter.is_cross_margin_bankrupt(), true); { let market = market_map.get_ref_mut(&0).unwrap(); @@ -2596,7 +2593,7 @@ pub mod delisting_test { assert_eq!(market.amm.cumulative_funding_rate_short, 0); let strict_quote_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (perp_margin_requirement, weighted_pnl, _, _, _) = + let (perp_margin_requirement, weighted_pnl, _, _) = calculate_perp_position_value_and_pnl( &shorter.perp_positions[0], &market, @@ -2605,7 +2602,6 @@ pub mod delisting_test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); diff --git a/programs/drift/src/controller/pnl/tests.rs b/programs/drift/src/controller/pnl/tests.rs index 4a35df4e49..e2f10ac990 100644 --- a/programs/drift/src/controller/pnl/tests.rs +++ b/programs/drift/src/controller/pnl/tests.rs @@ -22,7 +22,7 @@ use crate::state::perp_market_map::PerpMarketMap; use crate::state::spot_market::{SpotBalanceType, SpotMarket}; use crate::state::spot_market_map::SpotMarketMap; use crate::state::state::{OracleGuardRails, State, ValidityGuardRails}; -use crate::state::user::{PerpPosition, SpotPosition, User}; +use crate::state::user::{PerpPosition, PositionFlag, SpotPosition, User}; use crate::test_utils::*; use crate::test_utils::{get_positions, get_pyth_price, get_spot_positions}; use crate::{create_account_info, SettlePnlMode}; @@ -2113,3 +2113,271 @@ pub fn is_price_divergence_ok_on_invalid_oracle() { .is_price_divergence_ok_for_settle_pnl(oracle_price.agg.price) .unwrap()); } + +#[test] +pub fn isolated_perp_position_negative_pnl() { + let clock = Clock { + slot: 0, + epoch_start_timestamp: 0, + epoch: 0, + leader_schedule_epoch: 0, + unix_timestamp: 0, + }; + let state = State { + oracle_guard_rails: OracleGuardRails { + validity: ValidityGuardRails { + slots_before_stale_for_amm: 10, // 5s + slots_before_stale_for_margin: 120, // 60s + confidence_interval_max_size: 1000, + too_volatile_ratio: 5, + }, + ..OracleGuardRails::default() + }, + ..State::default() + }; + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock.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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + base_asset_amount_long: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData { + last_oracle_price: oracle_price.agg.price, + last_oracle_price_twap_5min: oracle_price.agg.price, + last_oracle_price_twap: oracle_price.agg.price, + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + number_of_users: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + pnl_pool: PoolBalance { + scaled_balance: (50 * SPOT_BALANCE_PRECISION), + market_index: QUOTE_SPOT_MARKET_INDEX, + ..PoolBalance::default() + }, + unrealized_pnl_maintenance_asset_weight: SPOT_WEIGHT_PRECISION.cast().unwrap(), + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut 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: 100 * SPOT_BALANCE_PRECISION, + historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User { + perp_positions: get_positions(PerpPosition { + market_index: 0, + quote_asset_amount: -50 * QUOTE_PRECISION_I64, + isolated_position_scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let authority = Pubkey::default(); + + let mut expected_user = user; + expected_user.perp_positions[0].quote_asset_amount = 0; + expected_user.settled_perp_pnl = -50 * QUOTE_PRECISION_I64; + expected_user.perp_positions[0].settled_pnl = -50 * QUOTE_PRECISION_I64; + expected_user.perp_positions[0].isolated_position_scaled_balance = + 50 * SPOT_BALANCE_PRECISION_U64; + + let mut expected_market = market; + expected_market.pnl_pool.scaled_balance = 100 * SPOT_BALANCE_PRECISION; + expected_market.amm.quote_asset_amount = -100 * QUOTE_PRECISION_I128; + expected_market.number_of_users = 0; + + settle_pnl( + 0, + &mut user, + &authority, + &user_key, + &market_map, + &spot_market_map, + &mut oracle_map, + &clock, + &state, + None, + SettlePnlMode::MustSettle, + ) + .unwrap(); + + assert_eq!(expected_user, user); + assert_eq!(expected_market, *market_map.get_ref(&0).unwrap()); +} + +#[test] +pub fn isolated_perp_position_user_unsettled_positive_pnl_less_than_pool() { + let clock = Clock { + slot: 0, + epoch_start_timestamp: 0, + epoch: 0, + leader_schedule_epoch: 0, + unix_timestamp: 0, + }; + let state = State { + oracle_guard_rails: OracleGuardRails { + validity: ValidityGuardRails { + slots_before_stale_for_amm: 10, // 5s + slots_before_stale_for_margin: 120, // 60s + confidence_interval_max_size: 1000, + too_volatile_ratio: 5, + }, + ..OracleGuardRails::default() + }, + ..State::default() + }; + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock.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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + base_asset_amount_long: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData { + last_oracle_price: oracle_price.agg.price, + last_oracle_price_twap_5min: oracle_price.agg.price, + last_oracle_price_twap: oracle_price.agg.price, + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + number_of_users: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + pnl_pool: PoolBalance { + scaled_balance: (50 * SPOT_BALANCE_PRECISION), + market_index: QUOTE_SPOT_MARKET_INDEX, + ..PoolBalance::default() + }, + unrealized_pnl_maintenance_asset_weight: SPOT_WEIGHT_PRECISION.cast().unwrap(), + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut 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: 100 * SPOT_BALANCE_PRECISION, + historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User { + perp_positions: get_positions(PerpPosition { + market_index: 0, + quote_asset_amount: 25 * QUOTE_PRECISION_I64, + isolated_position_scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let authority = Pubkey::default(); + + let mut expected_user = user; + expected_user.perp_positions[0].quote_asset_amount = 0; + expected_user.settled_perp_pnl = 25 * QUOTE_PRECISION_I64; + expected_user.perp_positions[0].settled_pnl = 25 * QUOTE_PRECISION_I64; + expected_user.perp_positions[0].isolated_position_scaled_balance = + 125 * SPOT_BALANCE_PRECISION_U64; + + let mut expected_market = market; + expected_market.pnl_pool.scaled_balance = 25 * SPOT_BALANCE_PRECISION; + expected_market.amm.quote_asset_amount = -175 * QUOTE_PRECISION_I128; + expected_market.number_of_users = 0; + + settle_pnl( + 0, + &mut user, + &authority, + &user_key, + &market_map, + &spot_market_map, + &mut oracle_map, + &clock, + &state, + None, + SettlePnlMode::MustSettle, + ) + .unwrap(); + + assert_eq!(expected_user, user); + assert_eq!(expected_market, *market_map.get_ref(&0).unwrap()); +} diff --git a/programs/drift/src/controller/position/tests.rs b/programs/drift/src/controller/position/tests.rs index 89f8305340..862703cad0 100644 --- a/programs/drift/src/controller/position/tests.rs +++ b/programs/drift/src/controller/position/tests.rs @@ -122,10 +122,11 @@ fn amm_pool_balance_liq_fees_example() { assert_eq!(new_total_fee_minus_distributions, 640881949608); let unsettled_pnl = -10_000_000; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut perp_market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) diff --git a/programs/drift/src/controller/spot_balance/tests.rs b/programs/drift/src/controller/spot_balance/tests.rs index 83d6603fff..a8158e5a73 100644 --- a/programs/drift/src/controller/spot_balance/tests.rs +++ b/programs/drift/src/controller/spot_balance/tests.rs @@ -8,6 +8,7 @@ use crate::controller::spot_balance::*; use crate::controller::spot_position::update_spot_balances_and_cumulative_deposits_with_limits; use crate::create_account_info; use crate::create_anchor_account_info; +use crate::error::ErrorCode; use crate::math::constants::{ AMM_RESERVE_PRECISION, BASE_PRECISION_I128, BASE_PRECISION_I64, LIQUIDATION_FEE_PRECISION, PEG_PRECISION, PRICE_PRECISION_I64, PRICE_PRECISION_U64, QUOTE_PRECISION, QUOTE_PRECISION_I128, @@ -31,6 +32,7 @@ use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; use crate::state::perp_market_map::PerpMarketMap; use crate::state::spot_market::{InsuranceFund, SpotBalanceType, SpotMarket}; use crate::state::spot_market_map::SpotMarketMap; +use crate::state::user::PositionFlag; use crate::state::user::{Order, PerpPosition, SpotPosition, User}; use crate::test_utils::*; use crate::test_utils::{get_pyth_price, get_spot_positions}; @@ -1955,3 +1957,71 @@ fn check_spot_market_min_borrow_rate() { assert_eq!(accum_interest.borrow_interest, 317107433); assert_eq!(accum_interest.deposit_interest, 3171074); } + +#[test] +fn isolated_perp_position() { + let now = 30_i64; + let _slot = 0_u64; + + let mut spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 100_000_000 * SPOT_BALANCE_PRECISION, //$100M usdc + borrow_balance: 0, + deposit_token_twap: QUOTE_PRECISION_U64 / 2, + historical_oracle_data: HistoricalOracleData::default_quote_oracle(), + status: MarketStatus::Active, + ..SpotMarket::default() + }; + + let mut perp_position = PerpPosition { + market_index: 0, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }; + + let amount = QUOTE_PRECISION; + + update_spot_balances( + amount, + &SpotBalanceType::Deposit, + &mut spot_market, + &mut perp_position, + false, + ) + .unwrap(); + + assert_eq!(perp_position.isolated_position_scaled_balance, 1000000000); + assert_eq!( + perp_position + .get_isolated_token_amount(&spot_market) + .unwrap(), + amount + ); + + update_spot_balances( + amount, + &SpotBalanceType::Borrow, + &mut spot_market, + &mut perp_position, + false, + ) + .unwrap(); + + assert_eq!(perp_position.isolated_position_scaled_balance, 0); + + let result = update_spot_balances( + amount, + &SpotBalanceType::Borrow, + &mut spot_market, + &mut perp_position, + false, + ); + + assert_eq!(result, Err(ErrorCode::CantUpdateSpotBalanceType)); +} diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index d09e3bfd1c..6610f6ccf1 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -193,8 +193,8 @@ pub enum ErrorCode { SpotMarketInsufficientDeposits, #[msg("UserMustSettleTheirOwnPositiveUnsettledPNL")] UserMustSettleTheirOwnPositiveUnsettledPNL, - #[msg("CantUpdatePoolBalanceType")] - CantUpdatePoolBalanceType, + #[msg("CantUpdateSpotBalanceType")] + CantUpdateSpotBalanceType, #[msg("InsufficientCollateralForSettlingPNL")] InsufficientCollateralForSettlingPNL, #[msg("AMMNotUpdatedInSameSlot")] @@ -639,6 +639,8 @@ pub enum ErrorCode { InvalidIfRebalanceConfig, #[msg("Invalid If Rebalance Swap")] InvalidIfRebalanceSwap, + #[msg("Invalid Isolated Perp Market")] + InvalidIsolatedPerpMarket, #[msg("Invalid RevenueShare resize")] InvalidRevenueShareResize, #[msg("Builder has been revoked")] diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 0fc1fc3850..cbccf7285c 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -3017,23 +3017,16 @@ pub fn handle_disable_user_high_leverage_mode<'c: 'info, 'info>( )?; if margin_calc.num_perp_liabilities > 0 { - let mut requires_invariant_check = false; - for position in user.perp_positions.iter().filter(|p| !p.is_available()) { let perp_market = perp_market_map.get_ref(&position.market_index)?; if perp_market.is_high_leverage_mode_enabled() { - requires_invariant_check = true; - break; // Exit early if invariant check is required + validate!( + margin_calc.meets_margin_requirement_with_buffer(), + ErrorCode::DefaultError, + "User does not meet margin requirement with buffer" + )?; } } - - if requires_invariant_check { - validate!( - margin_calc.meets_margin_requirement_with_buffer(), - ErrorCode::DefaultError, - "User does not meet margin requirement with buffer" - )?; - } } // only check if signer is not user authority @@ -3150,6 +3143,13 @@ pub fn handle_force_delete_user<'c: 'info, 'info>( None, None, None, + false, + )?; + + validate!( + !user.perp_positions.iter().any(|p| !p.is_available()), + ErrorCode::DefaultError, + "user must have no perp positions" )?; for spot_position in user.spot_positions.iter_mut() { diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 3cb9d8e41a..d69d68cbc7 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -16,6 +16,7 @@ use crate::controller::orders::{cancel_orders, ModifyOrderId}; use crate::controller::position::update_position_and_market; use crate::controller::position::PositionDirection; use crate::controller::spot_balance::update_revenue_pool_balances; +use crate::controller::spot_balance::update_spot_balances; use crate::controller::spot_position::{ update_spot_balances_and_cumulative_deposits, update_spot_balances_and_cumulative_deposits_with_limits, @@ -33,7 +34,8 @@ use crate::instructions::optional_accounts::{ }; use crate::instructions::SpotFulfillmentType; use crate::math::casting::Cast; -use crate::math::liquidation::is_user_being_liquidated; +use crate::math::liquidation::is_cross_margin_being_liquidated; +use crate::math::liquidation::is_isolated_margin_being_liquidated; use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_liability_info; use crate::math::margin::meets_initial_margin_requirement; use crate::math::margin::{ @@ -758,9 +760,9 @@ pub fn handle_deposit<'c: 'info, 'info>( } drop(spot_market); - if user.is_being_liquidated() { + if user.is_cross_margin_being_liquidated() { // try to update liquidation status if user is was already being liq'd - let is_being_liquidated = is_user_being_liquidated( + let is_being_liquidated = is_cross_margin_being_liquidated( user, &perp_market_map, &spot_market_map, @@ -769,7 +771,7 @@ pub fn handle_deposit<'c: 'info, 'info>( )?; if !is_being_liquidated { - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); } } @@ -935,8 +937,8 @@ pub fn handle_withdraw<'c: 'info, 'info>( validate_spot_margin_trading(user, &perp_market_map, &spot_market_map, &mut oracle_map)?; - if user.is_being_liquidated() { - user.exit_liquidation(); + if user.is_cross_margin_being_liquidated() { + user.exit_cross_margin_liquidation(); } user.update_last_active_slot(slot); @@ -1114,8 +1116,8 @@ pub fn handle_transfer_deposit<'c: 'info, 'info>( &mut oracle_map, )?; - if from_user.is_being_liquidated() { - from_user.exit_liquidation(); + if from_user.is_cross_margin_being_liquidated() { + from_user.exit_cross_margin_liquidation(); } from_user.update_last_active_slot(slot); @@ -1599,12 +1601,12 @@ pub fn handle_transfer_pools<'c: 'info, 'info>( to_user.update_last_active_slot(slot); - if from_user.is_being_liquidated() { - from_user.exit_liquidation(); + if from_user.is_cross_margin_being_liquidated() { + from_user.exit_cross_margin_liquidation(); } - if to_user.is_being_liquidated() { - to_user.exit_liquidation(); + if to_user.is_cross_margin_being_liquidated() { + to_user.exit_cross_margin_liquidation(); } let deposit_from_spot_market = spot_market_map.get_ref(&deposit_from_market_index)?; @@ -1911,14 +1913,16 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( ) }; + let mut from_user_margin_context = MarginContext::standard(MarginRequirementType::Maintenance) + .fuel_perp_delta(market_index, transfer_amount); + let from_user_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &from_user, &perp_market_map, &spot_market_map, &mut oracle_map, - MarginContext::standard(MarginRequirementType::Maintenance) - .fuel_perp_delta(market_index, transfer_amount), + from_user_margin_context, )?; validate!( @@ -1927,14 +1931,16 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( "from user margin requirement is greater than total collateral" )?; + let mut to_user_margin_context = MarginContext::standard(MarginRequirementType::Initial) + .fuel_perp_delta(market_index, -transfer_amount); + let to_user_margin_requirement = calculate_margin_requirement_and_total_collateral_and_liability_info( &to_user, &perp_market_map, &spot_market_map, &mut oracle_map, - MarginContext::standard(MarginRequirementType::Initial) - .fuel_perp_delta(market_index, -transfer_amount), + to_user_margin_context, )?; validate!( @@ -2045,6 +2051,218 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( Ok(()) } +#[access_control( + deposit_not_paused(&ctx.accounts.state) +)] +pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositIsolatedPerpPosition<'info>>, + spot_market_index: u16, + perp_market_index: u16, + amount: u64, +) -> Result<()> { + let user_key = ctx.accounts.user.key(); + let mut user = load_mut!(ctx.accounts.user)?; + + let state = &ctx.accounts.state; + let clock = Clock::get()?; + let now = clock.unix_timestamp; + let slot = clock.slot; + + 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(), + &get_writable_spot_market_set(spot_market_index), + clock.slot, + Some(state.oracle_guard_rails), + )?; + + let mint = get_token_mint(remaining_accounts_iter)?; + + controller::isolated_position::deposit_into_isolated_perp_position( + user_key, + &mut user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + state, + spot_market_index, + perp_market_index, + amount, + )?; + + let spot_market = spot_market_map.get_ref(&spot_market_index)?; + + controller::token::receive( + &ctx.accounts.token_program, + &ctx.accounts.user_token_account, + &ctx.accounts.spot_market_vault, + &ctx.accounts.authority, + amount, + &mint, + if spot_market.has_transfer_hook() { + Some(remaining_accounts_iter) + } else { + None + }, + )?; + + ctx.accounts.spot_market_vault.reload()?; + + math::spot_withdraw::validate_spot_market_vault_amount( + &spot_market, + ctx.accounts.spot_market_vault.amount, + )?; + + spot_market.validate_max_token_deposits_and_borrows(false)?; + + Ok(()) +} + +#[access_control( + deposit_not_paused(&ctx.accounts.state) + withdraw_not_paused(&ctx.accounts.state) +)] +pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, TransferIsolatedPerpPositionDeposit<'info>>, + spot_market_index: u16, + perp_market_index: u16, + amount: i64, +) -> anchor_lang::Result<()> { + let authority_key = ctx.accounts.authority.key; + let user_key = ctx.accounts.user.key(); + + let state = &ctx.accounts.state; + let clock = Clock::get()?; + let slot = clock.slot; + + let user = &mut load_mut!(ctx.accounts.user)?; + let user_stats = &mut load_mut!(ctx.accounts.user_stats)?; + + let clock = Clock::get()?; + let now = clock.unix_timestamp; + + validate!( + !user.is_bankrupt(), + ErrorCode::UserBankrupt, + "user bankrupt" + )?; + + let AccountMaps { + perp_market_map, + spot_market_map, + mut oracle_map, + } = load_maps( + &mut ctx.remaining_accounts.iter().peekable(), + &MarketSet::new(), + &get_writable_spot_market_set(spot_market_index), + clock.slot, + Some(state.oracle_guard_rails), + )?; + + controller::isolated_position::transfer_isolated_perp_position_deposit( + user, + user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + spot_market_index, + perp_market_index, + amount, + )?; + + let spot_market = spot_market_map.get_ref(&spot_market_index)?; + math::spot_withdraw::validate_spot_market_vault_amount( + &spot_market, + ctx.accounts.spot_market_vault.amount, + )?; + + Ok(()) +} + +#[access_control( + withdraw_not_paused(&ctx.accounts.state) +)] +pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, WithdrawIsolatedPerpPosition<'info>>, + spot_market_index: u16, + perp_market_index: u16, + amount: u64, +) -> anchor_lang::Result<()> { + let user_key = ctx.accounts.user.key(); + let user = &mut load_mut!(ctx.accounts.user)?; + let mut user_stats = load_mut!(ctx.accounts.user_stats)?; + let clock = Clock::get()?; + let now = clock.unix_timestamp; + let slot = clock.slot; + let state = &ctx.accounts.state; + + 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(), + &get_writable_spot_market_set(spot_market_index), + clock.slot, + Some(state.oracle_guard_rails), + )?; + + let mint = get_token_mint(remaining_accounts_iter)?; + + controller::isolated_position::withdraw_from_isolated_perp_position( + user_key, + user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + spot_market_index, + perp_market_index, + amount, + )?; + + let spot_market = spot_market_map.get_ref(&spot_market_index)?; + + controller::token::send_from_program_vault( + &ctx.accounts.token_program, + &ctx.accounts.spot_market_vault, + &ctx.accounts.user_token_account, + &ctx.accounts.drift_signer, + state.signer_nonce, + amount, + &mint, + if spot_market.has_transfer_hook() { + Some(remaining_accounts_iter) + } else { + None + }, + )?; + + // reload the spot market vault balance so it's up-to-date + ctx.accounts.spot_market_vault.reload()?; + math::spot_withdraw::validate_spot_market_vault_amount( + &spot_market, + ctx.accounts.spot_market_vault.amount, + )?; + + spot_market.validate_max_token_deposits_and_borrows(false)?; + + Ok(()) +} + #[access_control( exchange_not_paused(&ctx.accounts.state) )] @@ -2244,6 +2462,7 @@ pub fn handle_cancel_orders<'c: 'info, 'info>( market_type, market_index, direction, + false, )?; Ok(()) @@ -4538,6 +4757,92 @@ pub struct CancelOrder<'info> { pub authority: Signer<'info>, } +#[derive(Accounts)] +#[instruction(spot_market_index: u16,)] +pub struct DepositIsolatedPerpPosition<'info> { + pub state: Box>, + #[account( + mut, + constraint = can_sign_for_user(&user, &authority)? + )] + pub user: AccountLoader<'info, User>, + #[account( + mut, + constraint = is_stats_for_user(&user, &user_stats)? + )] + pub user_stats: AccountLoader<'info, UserStats>, + pub authority: Signer<'info>, + #[account( + mut, + seeds = [b"spot_market_vault".as_ref(), spot_market_index.to_le_bytes().as_ref()], + bump, + )] + pub spot_market_vault: Box>, + #[account( + mut, + constraint = &spot_market_vault.mint.eq(&user_token_account.mint), + token::authority = authority + )] + pub user_token_account: Box>, + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +#[instruction(spot_market_index: u16,)] +pub struct TransferIsolatedPerpPositionDeposit<'info> { + #[account( + mut, + constraint = can_sign_for_user(&user, &authority)? + )] + pub user: AccountLoader<'info, User>, + #[account( + mut, + has_one = authority + )] + pub user_stats: AccountLoader<'info, UserStats>, + pub authority: Signer<'info>, + pub state: Box>, + #[account( + seeds = [b"spot_market_vault".as_ref(), spot_market_index.to_le_bytes().as_ref()], + bump, + )] + pub spot_market_vault: Box>, +} + +#[derive(Accounts)] +#[instruction(spot_market_index: u16)] +pub struct WithdrawIsolatedPerpPosition<'info> { + pub state: Box>, + #[account( + mut, + has_one = authority, + )] + pub user: AccountLoader<'info, User>, + #[account( + mut, + has_one = authority + )] + pub user_stats: AccountLoader<'info, UserStats>, + pub authority: Signer<'info>, + #[account( + mut, + seeds = [b"spot_market_vault".as_ref(), spot_market_index.to_le_bytes().as_ref()], + bump, + )] + pub spot_market_vault: Box>, + #[account( + constraint = state.signer.eq(&drift_signer.key()) + )] + /// CHECK: forced drift_signer + pub drift_signer: AccountInfo<'info>, + #[account( + mut, + constraint = &spot_market_vault.mint.eq(&user_token_account.mint) + )] + pub user_token_account: Box>, + pub token_program: Interface<'info, TokenInterface>, +} + #[derive(Accounts)] pub struct PlaceAndTake<'info> { pub state: Box>, diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 42c9e9f050..11dddca369 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -195,6 +195,48 @@ pub mod drift { handle_transfer_perp_position(ctx, market_index, amount) } + pub fn deposit_into_isolated_perp_position<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositIsolatedPerpPosition<'info>>, + spot_market_index: u16, + perp_market_index: u16, + amount: u64, + ) -> Result<()> { + handle_deposit_into_isolated_perp_position( + ctx, + spot_market_index, + perp_market_index, + amount, + ) + } + + pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, TransferIsolatedPerpPositionDeposit<'info>>, + spot_market_index: u16, + perp_market_index: u16, + amount: i64, + ) -> Result<()> { + handle_transfer_isolated_perp_position_deposit( + ctx, + spot_market_index, + perp_market_index, + amount, + ) + } + + pub fn withdraw_from_isolated_perp_position<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, WithdrawIsolatedPerpPosition<'info>>, + spot_market_index: u16, + perp_market_index: u16, + amount: u64, + ) -> Result<()> { + handle_withdraw_from_isolated_perp_position( + ctx, + spot_market_index, + perp_market_index, + amount, + ) + } + pub fn place_perp_order<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, PlaceOrder>, params: OrderParams, diff --git a/programs/drift/src/math/bankruptcy.rs b/programs/drift/src/math/bankruptcy.rs index 287b103060..b6d3f97453 100644 --- a/programs/drift/src/math/bankruptcy.rs +++ b/programs/drift/src/math/bankruptcy.rs @@ -1,10 +1,11 @@ +use crate::error::DriftResult; use crate::state::spot_market::SpotBalanceType; use crate::state::user::User; #[cfg(test)] mod tests; -pub fn is_user_bankrupt(user: &User) -> bool { +pub fn is_cross_margin_bankrupt(user: &User) -> bool { // user is bankrupt iff they have spot liabilities, no spot assets, and no perp exposure let mut has_liability = false; @@ -33,3 +34,15 @@ pub fn is_user_bankrupt(user: &User) -> bool { has_liability } + +pub fn is_isolated_margin_bankrupt(user: &User, market_index: u16) -> DriftResult { + let perp_position = user.get_isolated_perp_position(market_index)?; + + if perp_position.isolated_position_scaled_balance > 0 { + return Ok(false); + } + + return Ok(perp_position.base_asset_amount == 0 + && perp_position.quote_asset_amount < 0 + && !perp_position.has_open_order()); +} diff --git a/programs/drift/src/math/bankruptcy/tests.rs b/programs/drift/src/math/bankruptcy/tests.rs index 01ba6aad7c..fbc745caf7 100644 --- a/programs/drift/src/math/bankruptcy/tests.rs +++ b/programs/drift/src/math/bankruptcy/tests.rs @@ -1,6 +1,6 @@ -use crate::math::bankruptcy::is_user_bankrupt; +use crate::math::bankruptcy::is_cross_margin_bankrupt; use crate::state::spot_market::SpotBalanceType; -use crate::state::user::{PerpPosition, SpotPosition, User}; +use crate::state::user::{PerpPosition, PositionFlag, SpotPosition, User}; use crate::test_utils::{get_positions, get_spot_positions}; #[test] @@ -13,7 +13,7 @@ fn user_has_position_with_base() { ..User::default() }; - let is_bankrupt = is_user_bankrupt(&user); + let is_bankrupt = is_cross_margin_bankrupt(&user); assert!(!is_bankrupt); } @@ -27,7 +27,7 @@ fn user_has_position_with_positive_quote() { ..User::default() }; - let is_bankrupt = is_user_bankrupt(&user); + let is_bankrupt = is_cross_margin_bankrupt(&user); assert!(!is_bankrupt); } @@ -42,7 +42,7 @@ fn user_with_deposit() { ..User::default() }; - let is_bankrupt = is_user_bankrupt(&user); + let is_bankrupt = is_cross_margin_bankrupt(&user); assert!(!is_bankrupt); } @@ -56,7 +56,7 @@ fn user_has_position_with_negative_quote() { ..User::default() }; - let is_bankrupt = is_user_bankrupt(&user); + let is_bankrupt = is_cross_margin_bankrupt(&user); assert!(is_bankrupt); } @@ -71,13 +71,55 @@ fn user_with_borrow() { ..User::default() }; - let is_bankrupt = is_user_bankrupt(&user); + let is_bankrupt = is_cross_margin_bankrupt(&user); assert!(is_bankrupt); } #[test] fn user_with_empty_position_and_balances() { let user = User::default(); - let is_bankrupt = is_user_bankrupt(&user); + let is_bankrupt = is_cross_margin_bankrupt(&user); assert!(!is_bankrupt); } + +#[test] +fn user_with_isolated_position() { + let user = User { + perp_positions: get_positions(PerpPosition { + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }), + ..User::default() + }; + + let mut user_with_scaled_balance = user.clone(); + user_with_scaled_balance.perp_positions[0].isolated_position_scaled_balance = + 1000000000000000000; + + let is_bankrupt = is_cross_margin_bankrupt(&user_with_scaled_balance); + assert!(!is_bankrupt); + + let mut user_with_base_asset_amount = user.clone(); + user_with_base_asset_amount.perp_positions[0].base_asset_amount = 1000000000000000000; + + let is_bankrupt = is_cross_margin_bankrupt(&user_with_base_asset_amount); + assert!(!is_bankrupt); + + let mut user_with_open_order = user.clone(); + user_with_open_order.perp_positions[0].open_orders = 1; + + let is_bankrupt = is_cross_margin_bankrupt(&user_with_open_order); + assert!(!is_bankrupt); + + let mut user_with_positive_pnl = user.clone(); + user_with_positive_pnl.perp_positions[0].quote_asset_amount = 1000000000000000000; + + let is_bankrupt = is_cross_margin_bankrupt(&user_with_positive_pnl); + assert!(!is_bankrupt); + + let mut user_with_negative_pnl = user.clone(); + user_with_negative_pnl.perp_positions[0].quote_asset_amount = -1000000000000000000; + + let is_bankrupt = is_cross_margin_bankrupt(&user_with_negative_pnl); + assert!(is_bankrupt); +} diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index 24a54afc59..c1adc5ff2d 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -198,7 +198,7 @@ pub fn calculate_asset_transfer_for_liability_transfer( Ok(asset_transfer) } -pub fn is_user_being_liquidated( +pub fn is_cross_margin_being_liquidated( user: &User, market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, @@ -213,7 +213,7 @@ pub fn is_user_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - let is_being_liquidated = !margin_calculation.can_exit_liquidation()?; + let is_being_liquidated = !margin_calculation.can_exit_cross_margin_liquidation()?; Ok(is_being_liquidated) } @@ -229,23 +229,62 @@ pub fn validate_user_not_being_liquidated( return Ok(()); } - let is_still_being_liquidated = is_user_being_liquidated( + let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, market_map, spot_market_map, oracle_map, - liquidation_margin_buffer_ratio, + MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if is_still_being_liquidated { - return Err(ErrorCode::UserIsBeingLiquidated); + if user.is_cross_margin_being_liquidated() { + if margin_calculation.can_exit_cross_margin_liquidation()? { + user.exit_cross_margin_liquidation(); + } else { + return Err(ErrorCode::UserIsBeingLiquidated); + } } else { - user.exit_liquidation() + let isolated_positions_being_liquidated = user + .perp_positions + .iter() + .filter(|position| position.is_isolated() && position.is_being_liquidated()) + .map(|position| position.market_index) + .collect::>(); + + for perp_market_index in isolated_positions_being_liquidated { + if margin_calculation.can_exit_isolated_margin_liquidation(perp_market_index)? { + user.exit_isolated_margin_liquidation(perp_market_index)?; + } else { + return Err(ErrorCode::UserIsBeingLiquidated); + } + } } Ok(()) } +pub fn is_isolated_margin_being_liquidated( + user: &User, + market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + perp_market_index: u16, + liquidation_margin_buffer_ratio: u32, +) -> DriftResult { + let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( + user, + market_map, + spot_market_map, + oracle_map, + MarginContext::liquidation(liquidation_margin_buffer_ratio), + )?; + + let is_being_liquidated = + !margin_calculation.can_exit_isolated_margin_liquidation(perp_market_index)?; + + Ok(is_being_liquidated) +} + pub enum LiquidationMultiplierType { Discount, Premium, diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 9d227bfe8b..65863c4a8a 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -30,6 +30,8 @@ use num_integer::Roots; use std::cmp::{max, min, Ordering}; use std::collections::BTreeMap; +use super::spot_balance::get_token_amount; + #[cfg(test)] mod tests; @@ -103,8 +105,7 @@ pub fn calculate_perp_position_value_and_pnl( margin_requirement_type: MarginRequirementType, user_custom_margin_ratio: u32, user_high_leverage_mode: bool, - track_open_order_fraction: bool, -) -> DriftResult<(u128, i128, u128, u128, u128)> { +) -> DriftResult<(u128, i128, u128, u128)> { let valuation_price = if market.status == MarketStatus::Settlement { market.expiry_price } else { @@ -181,22 +182,10 @@ pub fn calculate_perp_position_value_and_pnl( weighted_unrealized_pnl = weighted_unrealized_pnl.min(MAX_POSITIVE_UPNL_FOR_INITIAL_MARGIN); } - let open_order_margin_requirement = - if track_open_order_fraction && worst_case_base_asset_amount != 0 { - let worst_case_base_asset_amount = worst_case_base_asset_amount.unsigned_abs(); - worst_case_base_asset_amount - .safe_sub(market_position.base_asset_amount.unsigned_abs().cast()?)? - .safe_mul(margin_requirement)? - .safe_div(worst_case_base_asset_amount)? - } else { - 0_u128 - }; - Ok(( margin_requirement, weighted_unrealized_pnl, worse_case_liability_value, - open_order_margin_requirement, base_asset_value, )) } @@ -228,6 +217,7 @@ pub fn calculate_user_safest_position_tiers( Ok((safest_tier_spot_liablity, safest_tier_perp_liablity)) } +// todo make sure everything using this sets isolated_position_market_index correctly pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( user: &User, perp_market_map: &PerpMarketMap, @@ -323,7 +313,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( token_value = 0; } - calculation.add_total_collateral(token_value)?; + calculation.add_cross_margin_total_collateral(token_value)?; calculation.update_all_deposit_oracles_valid(oracle_valid); @@ -341,7 +331,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( spot_market.market_index, )?; - calculation.add_margin_requirement( + calculation.add_cross_margin_margin_requirement( token_value, token_value, MarketIdentifier::spot(0), @@ -394,7 +384,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( )?; } - calculation.add_margin_requirement( + calculation.add_cross_margin_margin_requirement( spot_position.margin_requirement_for_open_orders()?, 0, MarketIdentifier::spot(spot_market.market_index), @@ -410,8 +400,9 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( worst_case_weighted_token_value = 0; } - calculation - .add_total_collateral(worst_case_weighted_token_value.cast::()?)?; + calculation.add_cross_margin_total_collateral( + worst_case_weighted_token_value.cast::()?, + )?; calculation.update_all_deposit_oracles_valid(oracle_valid); @@ -434,7 +425,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( spot_market.market_index, )?; - calculation.add_margin_requirement( + calculation.add_cross_margin_margin_requirement( worst_case_weighted_token_value.unsigned_abs(), worst_case_token_value.unsigned_abs(), MarketIdentifier::spot(spot_market.market_index), @@ -471,13 +462,15 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( worst_case_orders_value = 0; } - calculation.add_total_collateral(worst_case_orders_value.cast::()?)?; + calculation.add_cross_margin_total_collateral( + worst_case_orders_value.cast::()?, + )?; #[cfg(feature = "drift-rs")] calculation.add_spot_asset_value(worst_case_orders_value)?; } Ordering::Less => { - calculation.add_margin_requirement( + calculation.add_cross_margin_margin_requirement( worst_case_orders_value.unsigned_abs(), worst_case_orders_value.unsigned_abs(), MarketIdentifier::spot(0), @@ -544,22 +537,16 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( 0_u32 }; - let ( - perp_margin_requirement, - weighted_pnl, - worst_case_liability_value, - open_order_margin_requirement, - base_asset_value, - ) = calculate_perp_position_value_and_pnl( - market_position, - market, - oracle_price_data, - &strict_quote_price, - context.margin_type, - user_custom_margin_ratio.max(perp_position_custom_margin_ratio), - user_high_leverage_mode, - calculation.track_open_orders_fraction(), - )?; + let (perp_margin_requirement, weighted_pnl, worst_case_liability_value, base_asset_value) = + calculate_perp_position_value_and_pnl( + market_position, + market, + oracle_price_data, + &strict_quote_price, + context.margin_type, + user_custom_margin_ratio.max(perp_position_custom_margin_ratio), + user_high_leverage_mode, + )?; calculation.update_fuel_perp_bonus( market, @@ -568,17 +555,41 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( oracle_price_data.price, )?; - calculation.add_margin_requirement( - perp_margin_requirement, - worst_case_liability_value, - MarketIdentifier::perp(market.market_index), - )?; + if market_position.is_isolated() { + let quote_spot_market = spot_market_map.get_ref(&market.quote_spot_market_index)?; + let quote_token_amount = get_token_amount( + market_position + .isolated_position_scaled_balance + .cast::()?, + "e_spot_market, + &SpotBalanceType::Deposit, + )?; - if calculation.track_open_orders_fraction() { - calculation.add_open_orders_margin_requirement(open_order_margin_requirement)?; - } + let quote_token_value = get_strict_token_value( + quote_token_amount.cast::()?, + quote_spot_market.decimals, + &strict_quote_price, + )?; + + calculation.add_isolated_margin_calculation( + market.market_index, + quote_token_value, + weighted_pnl, + worst_case_liability_value, + perp_margin_requirement, + )?; + + #[cfg(feature = "drift-rs")] + calculation.add_spot_asset_value(quote_token_value)?; + } else { + calculation.add_cross_margin_margin_requirement( + perp_margin_requirement, + worst_case_liability_value, + MarketIdentifier::perp(market.market_index), + )?; - calculation.add_total_collateral(weighted_pnl)?; + calculation.add_cross_margin_total_collateral(weighted_pnl)?; + } #[cfg(feature = "drift-rs")] calculation.add_perp_liability_value(worst_case_liability_value)?; @@ -645,7 +656,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( pub fn validate_any_isolated_tier_requirements( user: &User, - calculation: MarginCalculation, + calculation: &MarginCalculation, ) -> DriftResult { if calculation.with_perp_isolated_liability && !user.is_reduce_only() { validate!( @@ -694,27 +705,21 @@ pub fn meets_place_order_margin_requirement( } else { MarginRequirementType::Maintenance }; - let context = MarginContext::standard(margin_type).strict(true); let calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - context, + MarginContext::standard(margin_type).strict(true), )?; if !calculation.meets_margin_requirement() { - msg!( - "total_collateral={}, margin_requirement={} margin type = {:?}", - calculation.total_collateral, - calculation.margin_requirement, - margin_type - ); + msg!("margin calculation: {:?}", calculation); return Err(ErrorCode::InsufficientCollateral); } - validate_any_isolated_tier_requirements(user, calculation)?; + validate_any_isolated_tier_requirements(user, &calculation)?; Ok(()) } @@ -806,7 +811,7 @@ pub fn calculate_max_withdrawable_amount( return token_amount.cast(); } - let free_collateral = calculation.get_free_collateral()?; + let free_collateral = calculation.get_cross_free_collateral()?; let (numerator_scale, denominator_scale) = if spot_market.decimals > 6 { (10_u128.pow(spot_market.decimals - 6), 1) @@ -985,6 +990,19 @@ pub fn calculate_user_equity( all_oracles_valid &= is_oracle_valid_for_action(quote_oracle_validity, Some(DriftAction::MarginCalc))?; + if market_position.is_isolated() { + let quote_token_amount = + market_position.get_isolated_token_amount("e_spot_market)?; + + let token_value = get_token_value( + quote_token_amount.cast()?, + quote_spot_market.decimals, + quote_oracle_price_data.price, + )?; + + net_usd_value = net_usd_value.safe_add(token_value)?; + } + quote_oracle_price_data.price }; diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 2e9e25da44..9f997e11f3 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -286,7 +286,7 @@ mod test { assert_eq!(uaw, 9559); let strict_oracle_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (pmr, upnl, _, _, _) = calculate_perp_position_value_and_pnl( + let (pmr, upnl, _, _) = calculate_perp_position_value_and_pnl( &market_position, &market, &oracle_price_data, @@ -294,7 +294,6 @@ mod test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -364,7 +363,7 @@ mod test { assert_eq!(position_unrealized_pnl * 800000, 19426229516800000); // 1.9 billion let strict_oracle_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (pmr_2, upnl_2, _, _, _) = calculate_perp_position_value_and_pnl( + let (pmr_2, upnl_2, _, _) = calculate_perp_position_value_and_pnl( &market_position, &market, &oracle_price_data, @@ -372,7 +371,6 @@ mod test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -2923,7 +2921,7 @@ mod calculate_margin_requirement_and_total_collateral_and_liability_info { assert_eq!(calculation.total_collateral, 0); assert_eq!( - calculation.get_total_collateral_plus_buffer(), + calculation.get_cross_total_collateral_plus_buffer(), -QUOTE_PRECISION_I128 ); } @@ -4212,7 +4210,7 @@ mod calculate_perp_position_value_and_pnl_prediction_market { let strict_oracle_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (margin_requirement, upnl, _, _, _) = calculate_perp_position_value_and_pnl( + let (margin_requirement, upnl, _, _) = calculate_perp_position_value_and_pnl( &market_position, &market, &oracle_price_data, @@ -4220,14 +4218,13 @@ mod calculate_perp_position_value_and_pnl_prediction_market { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); assert_eq!(margin_requirement, QUOTE_PRECISION * 3 / 4); //$.75 assert_eq!(upnl, 0); //0 - let (margin_requirement, upnl, _, _, _) = calculate_perp_position_value_and_pnl( + let (margin_requirement, upnl, _, _) = calculate_perp_position_value_and_pnl( &market_position, &market, &oracle_price_data, @@ -4235,7 +4232,6 @@ mod calculate_perp_position_value_and_pnl_prediction_market { MarginRequirementType::Maintenance, 0, false, - false, ) .unwrap(); @@ -4276,7 +4272,7 @@ mod calculate_perp_position_value_and_pnl_prediction_market { let strict_oracle_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (margin_requirement, upnl, _, _, _) = calculate_perp_position_value_and_pnl( + let (margin_requirement, upnl, _, _) = calculate_perp_position_value_and_pnl( &market_position, &market, &oracle_price_data, @@ -4284,14 +4280,13 @@ mod calculate_perp_position_value_and_pnl_prediction_market { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); assert_eq!(margin_requirement, QUOTE_PRECISION * 3 / 4); //$.75 assert_eq!(upnl, 0); //0 - let (margin_requirement, upnl, _, _, _) = calculate_perp_position_value_and_pnl( + let (margin_requirement, upnl, _, _) = calculate_perp_position_value_and_pnl( &market_position, &market, &oracle_price_data, @@ -4299,7 +4294,6 @@ mod calculate_perp_position_value_and_pnl_prediction_market { MarginRequirementType::Maintenance, 0, false, - false, ) .unwrap(); @@ -4448,6 +4442,199 @@ mod pools { } } +#[cfg(test)] +mod isolated_position { + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::create_account_info; + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I64, LIQUIDATION_FEE_PRECISION, PEG_PRECISION, + SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, + SPOT_WEIGHT_PRECISION, + }; + use crate::math::margin::{ + calculate_margin_requirement_and_total_collateral_and_liability_info, MarginRequirementType, + }; + use crate::state::margin_calculation::{MarginCalculation, MarginContext}; + 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, PositionFlag, SpotPosition, User}; + use crate::test_utils::*; + use crate::test_utils::{get_positions, get_pyth_price}; + use crate::{create_anchor_account_info, QUOTE_PRECISION_I64}; + + #[test] + pub fn isolated_position_margin_requirement() { + 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 user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: 100 * BASE_PRECISION_I64, + quote_asset_amount: -11000 * QUOTE_PRECISION_I64, + position_flag: PositionFlag::IsolatedPosition as u8, + isolated_position_scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..PerpPosition::default() + }), + spot_positions, + ..User::default() + }; + + let margin_calculation = + calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial), + ) + .unwrap(); + + let cross_margin_margin_requirement = margin_calculation.margin_requirement; + let cross_total_collateral = margin_calculation.total_collateral; + + let isolated_margin_calculation = margin_calculation + .get_isolated_margin_calculation(0) + .unwrap(); + let isolated_margin_requirement = isolated_margin_calculation.margin_requirement; + let isolated_total_collateral = isolated_margin_calculation.total_collateral; + + assert_eq!(cross_margin_margin_requirement, 12000000000); + assert_eq!(cross_total_collateral, 20000000000); + assert_eq!(isolated_margin_requirement, 1000000000); + assert_eq!(isolated_total_collateral, -900000000); + assert_eq!(margin_calculation.meets_margin_requirement(), false); + assert_eq!(margin_calculation.meets_cross_margin_requirement(), true); + assert_eq!( + isolated_margin_calculation.meets_margin_requirement(), + false + ); + assert_eq!( + margin_calculation + .meets_isolated_margin_requirement(0) + .unwrap(), + false + ); + + let margin_calculation = + calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial).margin_buffer(1000), + ) + .unwrap(); + + let cross_margin_margin_requirement = margin_calculation.margin_requirement_plus_buffer; + let cross_total_collateral = margin_calculation.get_cross_total_collateral_plus_buffer(); + + let isolated_margin_calculation = margin_calculation + .get_isolated_margin_calculation(0) + .unwrap(); + let isolated_margin_requirement = + isolated_margin_calculation.margin_requirement_plus_buffer; + let isolated_total_collateral = + isolated_margin_calculation.get_total_collateral_plus_buffer(); + + assert_eq!(cross_margin_margin_requirement, 13000000000); + assert_eq!(cross_total_collateral, 20000000000); + assert_eq!(isolated_margin_requirement, 2000000000); + assert_eq!(isolated_total_collateral, -1000000000); + } +} + #[cfg(test)] mod get_margin_calculation_for_disable_high_leverage_mode { use std::str::FromStr; @@ -4455,7 +4642,6 @@ mod get_margin_calculation_for_disable_high_leverage_mode { use anchor_lang::Owner; use solana_program::pubkey::Pubkey; - use crate::create_anchor_account_info; 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, @@ -4470,7 +4656,7 @@ mod get_margin_calculation_for_disable_high_leverage_mode { use crate::state::user::{Order, PerpPosition, SpotPosition, User}; use crate::test_utils::get_pyth_price; use crate::test_utils::*; - use crate::{create_account_info, MARGIN_PRECISION}; + use crate::{create_account_info, create_anchor_account_info, MARGIN_PRECISION}; #[test] pub fn check_user_not_changed() { diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index b4495afd5c..b494c639b4 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -795,24 +795,30 @@ pub fn calculate_max_perp_order_size( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, ) -> DriftResult { + let mut margin_context = MarginContext::standard(MarginRequirementType::Initial).strict(true); // calculate initial margin requirement - let MarginCalculation { - margin_requirement, - total_collateral, - .. - } = calculate_margin_requirement_and_total_collateral_and_liability_info( + let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - MarginContext::standard(MarginRequirementType::Initial).strict(true), + margin_context, )?; let user_custom_margin_ratio = user.max_margin_ratio; let perp_position_margin_ratio = user.perp_positions[position_index].max_margin_ratio as u32; let user_high_leverage_mode = user.is_high_leverage_mode(MarginRequirementType::Initial); - let free_collateral_before = total_collateral.safe_sub(margin_requirement.cast()?)?; + let is_isolated_position = user.perp_positions[position_index].is_isolated(); + let free_collateral_before = if is_isolated_position { + margin_calculation + .get_isolated_free_collateral(market_index)? + .cast::()? + } else { + margin_calculation + .get_cross_free_collateral()? + .cast::()? + }; let perp_market = perp_market_map.get_ref(&market_index)?; diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index cdf97002e7..5bcb2492aa 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -438,6 +438,7 @@ pub struct LiquidationRecord { pub liquidate_perp_pnl_for_deposit: LiquidatePerpPnlForDepositRecord, pub perp_bankruptcy: PerpBankruptcyRecord, pub spot_bankruptcy: SpotBankruptcyRecord, + pub bit_flags: u8, } #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Eq, Default)] @@ -519,6 +520,11 @@ pub struct SpotBankruptcyRecord { pub cumulative_deposit_interest_delta: u128, } +#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] +pub enum LiquidationBitFlag { + IsolatedPosition = 0b00000001, +} + #[event] #[derive(Default)] pub struct SettlePnlRecord { diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs new file mode 100644 index 0000000000..d509973a0c --- /dev/null +++ b/programs/drift/src/state/liquidation_mode.rs @@ -0,0 +1,400 @@ +use solana_program::msg; + +use crate::{ + controller::{ + spot_balance::update_spot_balances, + spot_position::update_spot_balances_and_cumulative_deposits, + }, + error::{DriftResult, ErrorCode}, + math::{ + bankruptcy::{is_cross_margin_bankrupt, is_isolated_margin_bankrupt}, + liquidation::calculate_max_pct_to_liquidate, + margin::calculate_user_safest_position_tiers, + safe_unwrap::SafeUnwrap, + }, + state::margin_calculation::MarginCalculation, + validate, LIQUIDATION_PCT_PRECISION, QUOTE_SPOT_MARKET_INDEX, +}; + +use super::{ + events::LiquidationBitFlag, + perp_market::ContractTier, + perp_market_map::PerpMarketMap, + spot_market::{AssetTier, SpotBalanceType, SpotMarket}, + spot_market_map::SpotMarketMap, + user::{MarketType, User}, +}; + +pub trait LiquidatePerpMode { + fn user_is_being_liquidated(&self, user: &User) -> DriftResult; + + fn meets_margin_requirements( + &self, + margin_calculation: &MarginCalculation, + ) -> DriftResult; + + fn enter_liquidation(&self, user: &mut User, slot: u64) -> DriftResult; + + fn can_exit_liquidation(&self, margin_calculation: &MarginCalculation) -> DriftResult; + + fn exit_liquidation(&self, user: &mut User) -> DriftResult<()>; + + fn get_cancel_orders_params(&self) -> (Option, Option); + + fn calculate_max_pct_to_liquidate( + &self, + user: &User, + margin_shortage: u128, + slot: u64, + initial_pct_to_liquidate: u128, + liquidation_duration: u128, + ) -> DriftResult; + + fn increment_free_margin(&self, user: &mut User, amount: u64) -> DriftResult<()>; + + fn is_user_bankrupt(&self, user: &User) -> DriftResult; + + fn should_user_enter_bankruptcy(&self, user: &User) -> DriftResult; + + fn enter_bankruptcy(&self, user: &mut User) -> DriftResult<()>; + + fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()>; + + fn get_event_fields( + &self, + margin_calculation: &MarginCalculation, + ) -> DriftResult<(u128, i128, u8)>; + + fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()>; + + fn get_spot_token_amount(&self, user: &User, spot_market: &SpotMarket) -> DriftResult; + + fn calculate_user_safest_position_tiers( + &self, + user: &User, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + ) -> DriftResult<(AssetTier, ContractTier)>; + + fn decrease_spot_token_amount( + &self, + user: &mut User, + token_amount: u128, + spot_market: &mut SpotMarket, + cumulative_deposit_delta: Option, + ) -> DriftResult<()>; + + fn margin_shortage(&self, margin_calculation: &MarginCalculation) -> DriftResult; +} + +pub fn get_perp_liquidation_mode( + user: &User, + market_index: u16, +) -> DriftResult> { + let perp_position = user.get_perp_position(market_index)?; + let mode: Box = if perp_position.is_isolated() { + Box::new(IsolatedMarginLiquidatePerpMode::new(market_index)) + } else { + Box::new(CrossMarginLiquidatePerpMode::new(market_index)) + }; + + Ok(mode) +} + +pub struct CrossMarginLiquidatePerpMode { + pub market_index: u16, +} + +impl CrossMarginLiquidatePerpMode { + pub fn new(market_index: u16) -> Self { + Self { market_index } + } +} + +impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { + fn user_is_being_liquidated(&self, user: &User) -> DriftResult { + Ok(user.is_cross_margin_being_liquidated()) + } + + fn meets_margin_requirements( + &self, + margin_calculation: &MarginCalculation, + ) -> DriftResult { + Ok(margin_calculation.meets_cross_margin_requirement()) + } + + fn enter_liquidation(&self, user: &mut User, slot: u64) -> DriftResult { + user.enter_cross_margin_liquidation(slot) + } + + fn can_exit_liquidation(&self, margin_calculation: &MarginCalculation) -> DriftResult { + Ok(margin_calculation.can_exit_cross_margin_liquidation()?) + } + + fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { + Ok(user.exit_cross_margin_liquidation()) + } + + fn get_cancel_orders_params(&self) -> (Option, Option) { + (None, None) + } + + fn calculate_max_pct_to_liquidate( + &self, + user: &User, + margin_shortage: u128, + slot: u64, + initial_pct_to_liquidate: u128, + liquidation_duration: u128, + ) -> DriftResult { + calculate_max_pct_to_liquidate( + user, + margin_shortage, + slot, + initial_pct_to_liquidate, + liquidation_duration, + ) + } + + fn increment_free_margin(&self, user: &mut User, amount: u64) -> DriftResult<()> { + user.increment_margin_freed(amount) + } + + fn is_user_bankrupt(&self, user: &User) -> DriftResult { + Ok(user.is_cross_margin_bankrupt()) + } + + fn should_user_enter_bankruptcy(&self, user: &User) -> DriftResult { + Ok(is_cross_margin_bankrupt(user)) + } + + fn enter_bankruptcy(&self, user: &mut User) -> DriftResult<()> { + Ok(user.enter_cross_margin_bankruptcy()) + } + + fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()> { + Ok(user.exit_cross_margin_bankruptcy()) + } + + fn get_event_fields( + &self, + margin_calculation: &MarginCalculation, + ) -> DriftResult<(u128, i128, u8)> { + Ok(( + margin_calculation.margin_requirement, + margin_calculation.total_collateral, + 0, + )) + } + + fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()> { + if user.get_spot_position(asset_market_index).is_err() { + msg!( + "User does not have a spot balance for asset market {}", + asset_market_index + ); + + return Err(ErrorCode::CouldNotFindSpotPosition); + } + + Ok(()) + } + + fn get_spot_token_amount(&self, user: &User, spot_market: &SpotMarket) -> DriftResult { + let spot_position = user.get_spot_position(spot_market.market_index)?; + + validate!( + spot_position.balance_type == SpotBalanceType::Deposit, + ErrorCode::WrongSpotBalanceType, + "User did not have a deposit for the asset market" + )?; + + let token_amount = spot_position.get_token_amount(&spot_market)?; + + validate!( + token_amount != 0, + ErrorCode::InvalidSpotPosition, + "asset token amount zero for market index = {}", + spot_market.market_index + )?; + + Ok(token_amount) + } + + fn calculate_user_safest_position_tiers( + &self, + user: &User, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + ) -> DriftResult<(AssetTier, ContractTier)> { + calculate_user_safest_position_tiers(user, perp_market_map, spot_market_map) + } + + fn decrease_spot_token_amount( + &self, + user: &mut User, + token_amount: u128, + spot_market: &mut SpotMarket, + cumulative_deposit_delta: Option, + ) -> DriftResult<()> { + let spot_position = user.get_spot_position_mut(spot_market.market_index)?; + + update_spot_balances_and_cumulative_deposits( + token_amount, + &SpotBalanceType::Borrow, + spot_market, + spot_position, + false, + cumulative_deposit_delta, + )?; + + Ok(()) + } + + fn margin_shortage(&self, margin_calculation: &MarginCalculation) -> DriftResult { + margin_calculation.cross_margin_margin_shortage() + } +} + +pub struct IsolatedMarginLiquidatePerpMode { + pub market_index: u16, +} + +impl IsolatedMarginLiquidatePerpMode { + pub fn new(market_index: u16) -> Self { + Self { market_index } + } +} + +impl LiquidatePerpMode for IsolatedMarginLiquidatePerpMode { + fn user_is_being_liquidated(&self, user: &User) -> DriftResult { + user.is_isolated_margin_being_liquidated(self.market_index) + } + + fn meets_margin_requirements( + &self, + margin_calculation: &MarginCalculation, + ) -> DriftResult { + margin_calculation.meets_isolated_margin_requirement(self.market_index) + } + + fn can_exit_liquidation(&self, margin_calculation: &MarginCalculation) -> DriftResult { + margin_calculation.can_exit_isolated_margin_liquidation(self.market_index) + } + + fn enter_liquidation(&self, user: &mut User, slot: u64) -> DriftResult { + user.enter_isolated_margin_liquidation(self.market_index, slot) + } + + fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { + user.exit_isolated_margin_liquidation(self.market_index) + } + + fn get_cancel_orders_params(&self) -> (Option, Option) { + (Some(MarketType::Perp), Some(self.market_index)) + } + + fn calculate_max_pct_to_liquidate( + &self, + _user: &User, + _margin_shortage: u128, + _slot: u64, + _initial_pct_to_liquidate: u128, + _liquidation_duration: u128, + ) -> DriftResult { + Ok(LIQUIDATION_PCT_PRECISION) + } + + fn increment_free_margin(&self, user: &mut User, amount: u64) -> DriftResult<()> { + Ok(()) + } + + fn is_user_bankrupt(&self, user: &User) -> DriftResult { + user.is_isolated_margin_bankrupt(self.market_index) + } + + fn should_user_enter_bankruptcy(&self, user: &User) -> DriftResult { + is_isolated_margin_bankrupt(user, self.market_index) + } + + fn enter_bankruptcy(&self, user: &mut User) -> DriftResult<()> { + user.enter_isolated_margin_bankruptcy(self.market_index) + } + + fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()> { + user.exit_isolated_margin_bankruptcy(self.market_index) + } + + fn get_event_fields( + &self, + margin_calculation: &MarginCalculation, + ) -> DriftResult<(u128, i128, u8)> { + let isolated_margin_calculation = margin_calculation + .isolated_margin_calculations + .get(&self.market_index) + .safe_unwrap()?; + Ok(( + isolated_margin_calculation.margin_requirement, + isolated_margin_calculation.total_collateral, + LiquidationBitFlag::IsolatedPosition as u8, + )) + } + + fn validate_spot_position(&self, _user: &User, asset_market_index: u16) -> DriftResult<()> { + validate!( + asset_market_index == QUOTE_SPOT_MARKET_INDEX, + ErrorCode::CouldNotFindSpotPosition, + "asset market index must be quote asset market index for isolated liquidation mode" + ) + } + + fn get_spot_token_amount(&self, user: &User, spot_market: &SpotMarket) -> DriftResult { + let isolated_perp_position = user.get_isolated_perp_position(self.market_index)?; + + let token_amount = isolated_perp_position.get_isolated_token_amount(spot_market)?; + + validate!( + token_amount != 0, + ErrorCode::InvalidSpotPosition, + "asset token amount zero for market index = {}", + spot_market.market_index + )?; + + Ok(token_amount) + } + + fn calculate_user_safest_position_tiers( + &self, + _user: &User, + perp_market_map: &PerpMarketMap, + _spot_market_map: &SpotMarketMap, + ) -> DriftResult<(AssetTier, ContractTier)> { + let contract_tier = perp_market_map.get_ref(&self.market_index)?.contract_tier; + + Ok((AssetTier::default(), contract_tier)) + } + + fn decrease_spot_token_amount( + &self, + user: &mut User, + token_amount: u128, + spot_market: &mut SpotMarket, + _cumulative_deposit_delta: Option, + ) -> DriftResult<()> { + let perp_position = user.force_get_isolated_perp_position_mut(self.market_index)?; + + update_spot_balances( + token_amount, + &SpotBalanceType::Borrow, + spot_market, + perp_position, + false, + )?; + + Ok(()) + } + + fn margin_shortage(&self, margin_calculation: &MarginCalculation) -> DriftResult { + margin_calculation.isolated_margin_shortage(self.market_index) + } +} diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 4a0c299e4e..d4b19ad69f 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -1,8 +1,11 @@ +use std::collections::BTreeMap; + use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; use crate::math::fuel::{calculate_perp_fuel_bonus, calculate_spot_fuel_bonus}; use crate::math::margin::MarginRequirementType; use crate::math::safe_math::SafeMath; +use crate::math::safe_unwrap::SafeUnwrap; use crate::math::spot_balance::get_strict_token_value; use crate::state::oracle::StrictOraclePrice; use crate::state::perp_market::PerpMarket; @@ -15,9 +18,7 @@ use anchor_lang::{prelude::*, solana_program::msg}; #[derive(Clone, Copy, Debug)] pub enum MarginCalculationMode { - Standard { - track_open_orders_fraction: bool, - }, + Standard, Liquidation { market_to_track_margin_requirement: Option, }, @@ -63,9 +64,7 @@ impl MarginContext { pub fn standard(margin_type: MarginRequirementType) -> Self { Self { margin_type, - mode: MarginCalculationMode::Standard { - track_open_orders_fraction: false, - }, + mode: MarginCalculationMode::Standard, strict: false, ignore_invalid_deposit_oracles: false, margin_buffer: 0, @@ -114,21 +113,6 @@ impl MarginContext { self } - pub fn track_open_orders_fraction(mut self) -> DriftResult { - match self.mode { - MarginCalculationMode::Standard { - track_open_orders_fraction: ref mut track, - } => { - *track = true; - } - _ => { - msg!("Cant track open orders fraction outside of standard mode"); - return Err(ErrorCode::InvalidMarginCalculation); - } - } - Ok(self) - } - pub fn margin_ratio_override(mut self, margin_ratio_override: u32) -> Self { msg!( "Applying max margin ratio override: {} due to stale oracle", @@ -175,7 +159,7 @@ impl MarginContext { } } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub struct MarginCalculation { pub context: MarginContext, pub total_collateral: i128, @@ -188,6 +172,7 @@ pub struct MarginCalculation { margin_requirement_plus_buffer: u128, #[cfg(test)] pub margin_requirement_plus_buffer: u128, + pub isolated_margin_calculations: BTreeMap, pub num_spot_liabilities: u8, pub num_perp_liabilities: u8, pub all_deposit_oracles_valid: bool, @@ -198,13 +183,44 @@ pub struct MarginCalculation { pub total_spot_liability_value: u128, pub total_perp_liability_value: u128, pub total_perp_pnl: i128, - pub open_orders_margin_requirement: u128, tracked_market_margin_requirement: u128, pub fuel_deposits: u32, pub fuel_borrows: u32, pub fuel_positions: u32, } +#[derive(Clone, Copy, Debug, Default)] +pub struct IsolatedMarginCalculation { + pub margin_requirement: u128, + pub total_collateral: i128, + pub total_collateral_buffer: i128, + pub margin_requirement_plus_buffer: u128, +} + +impl IsolatedMarginCalculation { + pub fn get_total_collateral_plus_buffer(&self) -> i128 { + self.total_collateral + .saturating_add(self.total_collateral_buffer) + } + + pub fn meets_margin_requirement(&self) -> bool { + self.total_collateral >= self.margin_requirement as i128 + } + + pub fn meets_margin_requirement_with_buffer(&self) -> bool { + self.get_total_collateral_plus_buffer() >= self.margin_requirement_plus_buffer as i128 + } + + pub fn margin_shortage(&self) -> DriftResult { + Ok(self + .margin_requirement_plus_buffer + .cast::()? + .safe_sub(self.get_total_collateral_plus_buffer())? + .max(0) + .unsigned_abs()) + } +} + impl MarginCalculation { pub fn new(context: MarginContext) -> Self { Self { @@ -213,6 +229,7 @@ impl MarginCalculation { total_collateral_buffer: 0, margin_requirement: 0, margin_requirement_plus_buffer: 0, + isolated_margin_calculations: BTreeMap::new(), num_spot_liabilities: 0, num_perp_liabilities: 0, all_deposit_oracles_valid: true, @@ -223,7 +240,6 @@ impl MarginCalculation { total_spot_liability_value: 0, total_perp_liability_value: 0, total_perp_pnl: 0, - open_orders_margin_requirement: 0, tracked_market_margin_requirement: 0, fuel_deposits: 0, fuel_borrows: 0, @@ -231,7 +247,7 @@ impl MarginCalculation { } } - pub fn add_total_collateral(&mut self, total_collateral: i128) -> DriftResult { + pub fn add_cross_margin_total_collateral(&mut self, total_collateral: i128) -> DriftResult { self.total_collateral = self.total_collateral.safe_add(total_collateral)?; if self.context.margin_buffer > 0 && total_collateral < 0 { @@ -244,7 +260,7 @@ impl MarginCalculation { Ok(()) } - pub fn add_margin_requirement( + pub fn add_cross_margin_margin_requirement( &mut self, margin_requirement: u128, liability_value: u128, @@ -272,10 +288,48 @@ impl MarginCalculation { Ok(()) } - pub fn add_open_orders_margin_requirement(&mut self, margin_requirement: u128) -> DriftResult { - self.open_orders_margin_requirement = self - .open_orders_margin_requirement - .safe_add(margin_requirement)?; + pub fn add_isolated_margin_calculation( + &mut self, + market_index: u16, + deposit_value: i128, + pnl: i128, + liability_value: u128, + margin_requirement: u128, + ) -> DriftResult { + let total_collateral = deposit_value.safe_add(pnl)?; + + let total_collateral_buffer = if self.context.margin_buffer > 0 && pnl < 0 { + pnl.safe_mul(self.context.margin_buffer.cast::()?)? / MARGIN_PRECISION_I128 + } else { + 0 + }; + + let margin_requirement_plus_buffer = if self.context.margin_buffer > 0 { + margin_requirement.safe_add( + liability_value.safe_mul(self.context.margin_buffer)? / MARGIN_PRECISION_U128, + )? + } else { + 0 + }; + + let isolated_margin_calculation = IsolatedMarginCalculation { + margin_requirement, + total_collateral, + total_collateral_buffer, + margin_requirement_plus_buffer, + }; + + self.isolated_margin_calculations + .insert(market_index, isolated_margin_calculation); + + if let Some(market_to_track) = self.market_to_track_margin_requirement() { + if market_to_track == MarketIdentifier::perp(market_index) { + self.tracked_market_margin_requirement = self + .tracked_market_margin_requirement + .safe_add(margin_requirement)?; + } + } + Ok(()) } @@ -351,37 +405,98 @@ impl MarginCalculation { } #[inline(always)] - pub fn get_total_collateral_plus_buffer(&self) -> i128 { + pub fn get_cross_total_collateral_plus_buffer(&self) -> i128 { self.total_collateral .saturating_add(self.total_collateral_buffer) } pub fn meets_margin_requirement(&self) -> bool { - self.total_collateral >= self.margin_requirement as i128 + let cross_margin_meets_margin_requirement = self.meets_cross_margin_requirement(); + + if !cross_margin_meets_margin_requirement { + return false; + } + + for (_, isolated_margin_calculation) in &self.isolated_margin_calculations { + if !isolated_margin_calculation.meets_margin_requirement() { + return false; + } + } + + true } pub fn meets_margin_requirement_with_buffer(&self) -> bool { - self.get_total_collateral_plus_buffer() >= self.margin_requirement_plus_buffer as i128 + let cross_margin_meets_margin_requirement = + self.meets_cross_margin_requirement_with_buffer(); + + if !cross_margin_meets_margin_requirement { + return false; + } + + for (_, isolated_margin_calculation) in &self.isolated_margin_calculations { + if !isolated_margin_calculation.meets_margin_requirement_with_buffer() { + return false; + } + } + + true + } + + #[inline(always)] + pub fn meets_cross_margin_requirement(&self) -> bool { + self.total_collateral >= self.margin_requirement as i128 } - pub fn positions_meets_margin_requirement(&self) -> DriftResult { - Ok(self.total_collateral - >= self - .margin_requirement - .safe_sub(self.open_orders_margin_requirement)? - .cast::()?) + #[inline(always)] + pub fn meets_cross_margin_requirement_with_buffer(&self) -> bool { + self.get_cross_total_collateral_plus_buffer() >= self.margin_requirement_plus_buffer as i128 + } + + #[inline(always)] + pub fn meets_isolated_margin_requirement(&self, market_index: u16) -> DriftResult { + Ok(self + .isolated_margin_calculations + .get(&market_index) + .safe_unwrap()? + .meets_margin_requirement()) + } + + #[inline(always)] + pub fn meets_isolated_margin_requirement_with_buffer( + &self, + market_index: u16, + ) -> DriftResult { + Ok(self + .isolated_margin_calculations + .get(&market_index) + .safe_unwrap()? + .meets_margin_requirement_with_buffer()) } - pub fn can_exit_liquidation(&self) -> DriftResult { + pub fn can_exit_cross_margin_liquidation(&self) -> DriftResult { if !self.is_liquidation_mode() { msg!("liquidation mode not enabled"); return Err(ErrorCode::InvalidMarginCalculation); } - Ok(self.get_total_collateral_plus_buffer() >= self.margin_requirement_plus_buffer as i128) + Ok(self.meets_cross_margin_requirement_with_buffer()) } - pub fn margin_shortage(&self) -> DriftResult { + pub fn can_exit_isolated_margin_liquidation(&self, market_index: u16) -> DriftResult { + if !self.is_liquidation_mode() { + msg!("liquidation mode not enabled"); + return Err(ErrorCode::InvalidMarginCalculation); + } + + Ok(self + .isolated_margin_calculations + .get(&market_index) + .safe_unwrap()? + .meets_margin_requirement_with_buffer()) + } + + pub fn cross_margin_margin_shortage(&self) -> DriftResult { if self.context.margin_buffer == 0 { msg!("margin buffer mode not enabled"); return Err(ErrorCode::InvalidMarginCalculation); @@ -390,32 +505,76 @@ impl MarginCalculation { Ok(self .margin_requirement_plus_buffer .cast::()? - .safe_sub(self.get_total_collateral_plus_buffer())? + .safe_sub(self.get_cross_total_collateral_plus_buffer())? + .max(0) .unsigned_abs()) } - pub fn tracked_market_margin_shortage(&self, margin_shortage: u128) -> DriftResult { - if self.market_to_track_margin_requirement().is_none() { - msg!("cant call tracked_market_margin_shortage"); + pub fn isolated_margin_shortage(&self, market_index: u16) -> DriftResult { + if self.context.margin_buffer == 0 { + msg!("margin buffer mode not enabled"); return Err(ErrorCode::InvalidMarginCalculation); } - if self.margin_requirement == 0 { + self.isolated_margin_calculations + .get(&market_index) + .safe_unwrap()? + .margin_shortage() + } + + pub fn tracked_market_margin_shortage(&self, margin_shortage: u128) -> DriftResult { + let MarketIdentifier { + market_type, + market_index, + } = match self.market_to_track_margin_requirement() { + Some(market_to_track) => market_to_track, + None => { + msg!("no market to track margin requirement"); + return Err(ErrorCode::InvalidMarginCalculation); + } + }; + + let margin_requirement = if market_type == MarketType::Perp { + match self.isolated_margin_calculations.get(&market_index) { + Some(isolated_margin_calculation) => isolated_margin_calculation.margin_requirement, + None => self.margin_requirement, + } + } else { + self.margin_requirement + }; + + if margin_requirement == 0 { return Ok(0); } margin_shortage .safe_mul(self.tracked_market_margin_requirement)? - .safe_div(self.margin_requirement) + .safe_div(margin_requirement) } - pub fn get_free_collateral(&self) -> DriftResult { + pub fn get_cross_free_collateral(&self) -> DriftResult { self.total_collateral .safe_sub(self.margin_requirement.cast::()?)? .max(0) .cast() } + pub fn get_isolated_free_collateral(&self, market_index: u16) -> DriftResult { + let isolated_margin_calculation = self + .isolated_margin_calculations + .get(&market_index) + .safe_unwrap()?; + isolated_margin_calculation + .total_collateral + .safe_sub( + isolated_margin_calculation + .margin_requirement + .cast::()?, + )? + .max(0) + .cast() + } + fn market_to_track_margin_requirement(&self) -> Option { if let MarginCalculationMode::Liquidation { market_to_track_margin_requirement: track_margin_requirement, @@ -432,15 +591,6 @@ impl MarginCalculation { matches!(self.context.mode, MarginCalculationMode::Liquidation { .. }) } - pub fn track_open_orders_fraction(&self) -> bool { - matches!( - self.context.mode, - MarginCalculationMode::Standard { - track_open_orders_fraction: true - } - ) - } - pub fn update_fuel_perp_bonus( &mut self, perp_market: &PerpMarket, @@ -527,4 +677,22 @@ impl MarginCalculation { Ok(()) } + + pub fn get_isolated_margin_calculation( + &self, + market_index: u16, + ) -> DriftResult<&IsolatedMarginCalculation> { + if let Some(isolated_margin_calculation) = + self.isolated_margin_calculations.get(&market_index) + { + Ok(isolated_margin_calculation) + } else { + Err(ErrorCode::InvalidMarginCalculation) + } + } + + pub fn has_isolated_margin_calculation(&self, market_index: u16) -> bool { + self.isolated_margin_calculations + .contains_key(&market_index) + } } diff --git a/programs/drift/src/state/mod.rs b/programs/drift/src/state/mod.rs index 9237c008cd..3a938d0505 100644 --- a/programs/drift/src/state/mod.rs +++ b/programs/drift/src/state/mod.rs @@ -5,6 +5,7 @@ pub mod fulfillment_params; pub mod high_leverage_mode_config; pub mod if_rebalance_config; pub mod insurance_fund_stake; +pub mod liquidation_mode; pub mod load_ref; pub mod margin_calculation; pub mod oracle; diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 520af20258..12d621b545 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -945,7 +945,7 @@ impl SpotBalance for PoolBalance { } fn update_balance_type(&mut self, _balance_type: SpotBalanceType) -> DriftResult { - Err(ErrorCode::CantUpdatePoolBalanceType) + Err(ErrorCode::CantUpdateSpotBalanceType) } } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index b36607e3dc..5d0d22c63c 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -136,10 +136,18 @@ pub struct User { impl User { pub fn is_being_liquidated(&self) -> bool { + self.is_cross_margin_being_liquidated() || self.has_isolated_margin_being_liquidated() + } + + pub fn is_cross_margin_being_liquidated(&self) -> bool { self.status & (UserStatus::BeingLiquidated as u8 | UserStatus::Bankrupt as u8) > 0 } pub fn is_bankrupt(&self) -> bool { + self.is_cross_margin_bankrupt() || self.has_isolated_margin_bankrupt() + } + + pub fn is_cross_margin_bankrupt(&self) -> bool { self.status & (UserStatus::Bankrupt as u8) > 0 } @@ -258,6 +266,40 @@ impl User { Ok(&mut self.perp_positions[position_index]) } + pub fn force_get_isolated_perp_position_mut( + &mut self, + perp_market_index: u16, + ) -> DriftResult<&mut PerpPosition> { + if let Ok(position_index) = get_position_index(&self.perp_positions, perp_market_index) { + let perp_position = &mut self.perp_positions[position_index]; + validate!( + perp_position.is_isolated(), + ErrorCode::InvalidPerpPosition, + "perp position is not isolated" + )?; + + Ok(&mut self.perp_positions[position_index]) + } else { + let position_index = add_new_position(&mut self.perp_positions, perp_market_index)?; + + let perp_position = &mut self.perp_positions[position_index]; + perp_position.position_flag = PositionFlag::IsolatedPosition as u8; + + Ok(&mut self.perp_positions[position_index]) + } + } + + pub fn get_isolated_perp_position(&self, perp_market_index: u16) -> DriftResult<&PerpPosition> { + let position_index = get_position_index(&self.perp_positions, perp_market_index)?; + validate!( + self.perp_positions[position_index].is_isolated(), + ErrorCode::InvalidPerpPosition, + "perp position is not isolated" + )?; + + Ok(&self.perp_positions[position_index]) + } + pub fn get_order_index(&self, order_id: u32) -> DriftResult { self.orders .iter() @@ -336,34 +378,111 @@ impl User { Ok(()) } - pub fn enter_liquidation(&mut self, slot: u64) -> DriftResult { - if self.is_being_liquidated() { + pub fn enter_cross_margin_liquidation(&mut self, slot: u64) -> DriftResult { + if self.is_cross_margin_being_liquidated() { return self.next_liquidation_id.safe_sub(1); } self.add_user_status(UserStatus::BeingLiquidated); self.liquidation_margin_freed = 0; - self.last_active_slot = slot; - Ok(get_then_update_id!(self, next_liquidation_id)) + + let liquidation_id = if self.has_isolated_margin_being_liquidated() { + self.next_liquidation_id.safe_sub(1)? + } else { + self.last_active_slot = slot; + get_then_update_id!(self, next_liquidation_id) + }; + + Ok(liquidation_id) } - pub fn exit_liquidation(&mut self) { + pub fn exit_cross_margin_liquidation(&mut self) { self.remove_user_status(UserStatus::BeingLiquidated); self.remove_user_status(UserStatus::Bankrupt); self.liquidation_margin_freed = 0; } - pub fn enter_bankruptcy(&mut self) { + pub fn enter_cross_margin_bankruptcy(&mut self) { self.remove_user_status(UserStatus::BeingLiquidated); self.add_user_status(UserStatus::Bankrupt); } - pub fn exit_bankruptcy(&mut self) { + pub fn exit_cross_margin_bankruptcy(&mut self) { self.remove_user_status(UserStatus::BeingLiquidated); self.remove_user_status(UserStatus::Bankrupt); self.liquidation_margin_freed = 0; } + pub fn has_isolated_margin_being_liquidated(&self) -> bool { + self.perp_positions + .iter() + .any(|position| position.is_isolated() && position.is_being_liquidated()) + } + + pub fn enter_isolated_margin_liquidation( + &mut self, + perp_market_index: u16, + slot: u64, + ) -> DriftResult { + if self.is_isolated_margin_being_liquidated(perp_market_index)? { + return self.next_liquidation_id.safe_sub(1); + } + + let liquidation_id = if self.is_cross_margin_being_liquidated() + || self.has_isolated_margin_being_liquidated() + { + self.next_liquidation_id.safe_sub(1)? + } else { + self.last_active_slot = slot; + get_then_update_id!(self, next_liquidation_id) + }; + + let perp_position = self.force_get_isolated_perp_position_mut(perp_market_index)?; + + perp_position.position_flag |= PositionFlag::BeingLiquidated as u8; + + Ok(liquidation_id) + } + + pub fn exit_isolated_margin_liquidation(&mut self, perp_market_index: u16) -> DriftResult { + let perp_position = self.force_get_isolated_perp_position_mut(perp_market_index)?; + perp_position.position_flag &= !(PositionFlag::BeingLiquidated as u8); + perp_position.position_flag &= !(PositionFlag::Bankrupt as u8); + Ok(()) + } + + pub fn is_isolated_margin_being_liquidated(&self, perp_market_index: u16) -> DriftResult { + if let Ok(perp_position) = self.get_isolated_perp_position(perp_market_index) { + Ok(perp_position.is_being_liquidated()) + } else { + Ok(false) + } + } + + pub fn has_isolated_margin_bankrupt(&self) -> bool { + self.perp_positions + .iter() + .any(|position| position.is_isolated() && position.is_bankrupt()) + } + + pub fn enter_isolated_margin_bankruptcy(&mut self, perp_market_index: u16) -> DriftResult { + let perp_position = self.force_get_isolated_perp_position_mut(perp_market_index)?; + perp_position.position_flag &= !(PositionFlag::BeingLiquidated as u8); + perp_position.position_flag |= PositionFlag::Bankrupt as u8; + Ok(()) + } + + pub fn exit_isolated_margin_bankruptcy(&mut self, perp_market_index: u16) -> DriftResult { + let perp_position = self.force_get_isolated_perp_position_mut(perp_market_index)?; + perp_position.position_flag &= !(PositionFlag::Bankrupt as u8); + Ok(()) + } + + pub fn is_isolated_margin_bankrupt(&self, perp_market_index: u16) -> DriftResult { + let perp_position = self.get_isolated_perp_position(perp_market_index)?; + Ok(perp_position.position_flag & (PositionFlag::Bankrupt as u8) != 0) + } + pub fn increment_margin_freed(&mut self, margin_free: u64) -> DriftResult { self.liquidation_margin_freed = self.liquidation_margin_freed.safe_add(margin_free)?; Ok(()) @@ -536,14 +655,13 @@ impl User { )?; } - validate_any_isolated_tier_requirements(self, calculation)?; + validate_any_isolated_tier_requirements(self, &calculation)?; validate!( calculation.meets_margin_requirement(), ErrorCode::InsufficientCollateral, - "User attempting to withdraw where total_collateral {} is below initial_margin_requirement {}", - calculation.total_collateral, - calculation.margin_requirement + "margin calculation: {:?}", + calculation )?; user_stats.update_fuel_bonus( @@ -591,14 +709,13 @@ impl User { )?; } - validate_any_isolated_tier_requirements(self, calculation)?; + validate_any_isolated_tier_requirements(self, &calculation)?; validate!( calculation.meets_margin_requirement(), ErrorCode::InsufficientCollateral, - "User attempting to withdraw where total_collateral {} is below initial_margin_requirement {}", - calculation.total_collateral, - calculation.margin_requirement + "margin calculation: {:?}", + calculation )?; user_stats.update_fuel_bonus( @@ -974,8 +1091,8 @@ pub struct PerpPosition { pub lp_shares: u64, /// The last base asset amount per lp the amm had /// Used to settle the users lp position - /// precision: BASE_PRECISION - pub last_base_asset_amount_per_lp: i64, + /// precision: SPOT_BALANCE_PRECISION + pub isolated_position_scaled_balance: u64, /// The last quote asset amount per lp the amm had /// Used to settle the users lp position /// precision: QUOTE_PRECISION @@ -987,7 +1104,7 @@ pub struct PerpPosition { pub market_index: u16, /// The number of open orders pub open_orders: u8, - pub per_lp_base: i8, + pub position_flag: u8, } impl PerpPosition { @@ -996,7 +1113,11 @@ impl PerpPosition { } pub fn is_available(&self) -> bool { - !self.is_open_position() && !self.has_open_order() && !self.has_unsettled_pnl() + !self.is_open_position() + && !self.has_open_order() + && !self.has_unsettled_pnl() + && self.isolated_position_scaled_balance == 0 + && !self.is_being_liquidated() } pub fn is_open_position(&self) -> bool { @@ -1140,6 +1261,59 @@ impl PerpPosition { None } } + + pub fn is_isolated(&self) -> bool { + self.position_flag & PositionFlag::IsolatedPosition as u8 > 0 + } + + pub fn get_isolated_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { + get_token_amount( + self.isolated_position_scaled_balance as u128, + spot_market, + &SpotBalanceType::Deposit, + ) + } + + pub fn is_being_liquidated(&self) -> bool { + self.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankrupt as u8) + > 0 + } + + pub fn is_bankrupt(&self) -> bool { + self.position_flag & PositionFlag::Bankrupt as u8 > 0 + } +} + +impl SpotBalance for PerpPosition { + fn market_index(&self) -> u16 { + QUOTE_SPOT_MARKET_INDEX + } + + fn balance_type(&self) -> &SpotBalanceType { + &SpotBalanceType::Deposit + } + + fn balance(&self) -> u128 { + self.isolated_position_scaled_balance as u128 + } + + fn increase_balance(&mut self, delta: u128) -> DriftResult { + self.isolated_position_scaled_balance = self + .isolated_position_scaled_balance + .safe_add(delta.cast::()?)?; + Ok(()) + } + + fn decrease_balance(&mut self, delta: u128) -> DriftResult { + self.isolated_position_scaled_balance = self + .isolated_position_scaled_balance + .safe_sub(delta.cast::()?)?; + Ok(()) + } + + fn update_balance_type(&mut self, _balance_type: SpotBalanceType) -> DriftResult { + Err(ErrorCode::CantUpdateSpotBalanceType) + } } pub(crate) type PerpPositions = [PerpPosition; 8]; @@ -1619,6 +1793,13 @@ pub enum OrderBitFlag { HasBuilder = 0b00010000, } +#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] +pub enum PositionFlag { + IsolatedPosition = 0b00000001, + BeingLiquidated = 0b00000010, + Bankrupt = 0b00000100, +} + #[account(zero_copy(unsafe))] #[derive(Eq, PartialEq, Debug)] #[repr(C)] diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 5bd2b154f5..c4e4351acc 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -1679,36 +1679,36 @@ mod update_user_status { let mut user = User::default(); assert_eq!(user.status, 0); - user.enter_liquidation(0).unwrap(); + user.enter_cross_margin_liquidation(0).unwrap(); assert_eq!(user.status, UserStatus::BeingLiquidated as u8); - assert!(user.is_being_liquidated()); + assert!(user.is_cross_margin_being_liquidated()); - user.enter_bankruptcy(); + user.enter_cross_margin_bankruptcy(); assert_eq!(user.status, UserStatus::Bankrupt as u8); - assert!(user.is_being_liquidated()); - assert!(user.is_bankrupt()); + assert!(user.is_cross_margin_being_liquidated()); + assert!(user.is_cross_margin_bankrupt()); let mut user = User { status: UserStatus::ReduceOnly as u8, ..User::default() }; - user.enter_liquidation(0).unwrap(); + user.enter_cross_margin_liquidation(0).unwrap(); - assert!(user.is_being_liquidated()); + assert!(user.is_cross_margin_being_liquidated()); assert!(user.status & UserStatus::ReduceOnly as u8 > 0); - user.enter_bankruptcy(); + user.enter_cross_margin_bankruptcy(); - assert!(user.is_being_liquidated()); - assert!(user.is_bankrupt()); + assert!(user.is_cross_margin_being_liquidated()); + assert!(user.is_cross_margin_bankrupt()); assert!(user.status & UserStatus::ReduceOnly as u8 > 0); - user.exit_liquidation(); - assert!(!user.is_being_liquidated()); - assert!(!user.is_bankrupt()); + user.exit_cross_margin_liquidation(); + assert!(!user.is_cross_margin_being_liquidated()); + assert!(!user.is_cross_margin_bankrupt()); assert!(user.status & UserStatus::ReduceOnly as u8 > 0); } } @@ -2318,6 +2318,336 @@ mod update_referrer_status { } } +mod next_liquidation_id { + use crate::state::user::{PerpPosition, PositionFlag, User}; + + #[test] + fn test() { + let mut user = User::default(); + user.next_liquidation_id = 1; + let isolated_position = PerpPosition { + market_index: 1, + position_flag: PositionFlag::IsolatedPosition as u8, + base_asset_amount: 1, + ..PerpPosition::default() + }; + user.perp_positions[0] = isolated_position; + let isolated_position_2 = PerpPosition { + market_index: 2, + position_flag: PositionFlag::IsolatedPosition as u8, + base_asset_amount: 1, + ..PerpPosition::default() + }; + user.perp_positions[1] = isolated_position_2; + + let liquidation_id = user.enter_cross_margin_liquidation(2).unwrap(); + assert_eq!(liquidation_id, 1); + assert_eq!(user.last_active_slot, 2); + + let liquidation_id = user.enter_isolated_margin_liquidation(1, 3).unwrap(); + assert_eq!(liquidation_id, 1); + assert_eq!(user.last_active_slot, 2); + + user.exit_isolated_margin_liquidation(1).unwrap(); + + user.exit_cross_margin_liquidation(); + + let liquidation_id = user.enter_isolated_margin_liquidation(1, 4).unwrap(); + assert_eq!(liquidation_id, 2); + assert_eq!(user.last_active_slot, 4); + + let liquidation_id = user.enter_isolated_margin_liquidation(2, 5).unwrap(); + assert_eq!(liquidation_id, 2); + assert_eq!(user.last_active_slot, 4); + + let liquidation_id = user.enter_cross_margin_liquidation(6).unwrap(); + assert_eq!(liquidation_id, 2); + assert_eq!(user.last_active_slot, 4); + } +} + +mod force_get_isolated_perp_position_mut { + use crate::state::user::{PerpPosition, PositionFlag, User}; + + #[test] + fn test() { + let mut user = User::default(); + + let isolated_position = PerpPosition { + market_index: 1, + position_flag: PositionFlag::IsolatedPosition as u8, + base_asset_amount: 1, + ..PerpPosition::default() + }; + user.perp_positions[0] = isolated_position; + + { + let isolated_position_mut = user.force_get_isolated_perp_position_mut(1).unwrap(); + assert_eq!(isolated_position_mut.base_asset_amount, 1); + } + + { + let isolated_position = user.get_isolated_perp_position(1).unwrap(); + assert_eq!(isolated_position.base_asset_amount, 1); + } + + { + let isolated_position = user.get_isolated_perp_position(2); + assert_eq!(isolated_position.is_err(), true); + } + + { + let isolated_position_mut = user.force_get_isolated_perp_position_mut(2).unwrap(); + assert_eq!(isolated_position_mut.market_index, 2); + assert_eq!( + isolated_position_mut.position_flag, + PositionFlag::IsolatedPosition as u8 + ); + } + + let isolated_position = PerpPosition { + market_index: 1, + base_asset_amount: 1, + ..PerpPosition::default() + }; + + user.perp_positions[0] = isolated_position; + + { + let isolated_position_mut = user.force_get_isolated_perp_position_mut(1); + assert_eq!(isolated_position_mut.is_err(), true); + } + } +} + +pub mod meets_withdraw_margin_requirement_and_increment_fuel_bonus { + use crate::math::constants::ONE_HOUR; + use crate::state::state::State; + use std::collections::BTreeSet; + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::controller::liquidation::{liquidate_perp, liquidate_spot}; + use crate::controller::position::PositionDirection; + use crate::create_anchor_account_info; + use crate::error::ErrorCode; + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I128, BASE_PRECISION_I64, BASE_PRECISION_U64, + LIQUIDATION_FEE_PRECISION, LIQUIDATION_PCT_PRECISION, MARGIN_PRECISION, + MARGIN_PRECISION_U128, PEG_PRECISION, PRICE_PRECISION, PRICE_PRECISION_U64, + QUOTE_PRECISION, QUOTE_PRECISION_I128, QUOTE_PRECISION_I64, SPOT_BALANCE_PRECISION_U64, + SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, + }; + use crate::math::liquidation::is_cross_margin_being_liquidated; + use crate::math::margin::{ + calculate_margin_requirement_and_total_collateral_and_liability_info, MarginRequirementType, + }; + use crate::math::position::calculate_base_asset_value_with_oracle_price; + use crate::state::margin_calculation::{MarginCalculation, MarginContext}; + 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::{ + MarginMode, Order, OrderStatus, OrderType, PerpPosition, PositionFlag, SpotPosition, User, + UserStats, + }; + use crate::test_utils::*; + use crate::test_utils::{get_orders, get_positions, get_pyth_price, get_spot_positions}; + use crate::{create_account_info, PRICE_PRECISION_I64}; + + #[test] + pub fn unhealthy_isolated_perp_blocks_withdraw() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &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, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + + let mut market2 = market.clone(); + market2.market_index = 1; + create_anchor_account_info!(market2, PerpMarket, market2_account_info); + + let market_account_infos = vec![market_account_info, market2_account_info]; + let market_set = BTreeSet::default(); + let perp_market_map = + PerpMarketMap::load(&market_set, &mut market_account_infos.iter().peekable()).unwrap(); + + let mut spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + initial_liability_weight: SPOT_WEIGHT_PRECISION, + maintenance_liability_weight: SPOT_WEIGHT_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + + let mut spot_market2 = spot_market.clone(); + spot_market2.market_index = 1; + create_anchor_account_info!(spot_market2, SpotMarket, spot_market2_account_info); + + let spot_market_account_infos = vec![spot_market_account_info, spot_market2_account_info]; + let mut spot_market_set = BTreeSet::default(); + spot_market_set.insert(0); + spot_market_set.insert(1); + let spot_market_map = SpotMarketMap::load( + &spot_market_set, + &mut spot_market_account_infos.iter().peekable(), + ) + .unwrap(); + + let mut user = User { + orders: get_orders(Order { + market_index: 0, + status: OrderStatus::Open, + order_type: OrderType::Limit, + direction: PositionDirection::Long, + base_asset_amount: BASE_PRECISION_U64, + slot: 0, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: BASE_PRECISION_I64, + quote_asset_amount: -150 * QUOTE_PRECISION_I64, + quote_entry_amount: -150 * QUOTE_PRECISION_I64, + quote_break_even_amount: -150 * QUOTE_PRECISION_I64, + open_orders: 1, + open_bids: BASE_PRECISION_I64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }), + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + + ..User::default() + }; + + user.spot_positions[1] = SpotPosition { + market_index: 1, + balance_type: SpotBalanceType::Borrow, + scaled_balance: 1 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + + user.perp_positions[1] = PerpPosition { + market_index: 1, + base_asset_amount: BASE_PRECISION_I64, + quote_asset_amount: -100 * QUOTE_PRECISION_I64, + quote_entry_amount: -100 * QUOTE_PRECISION_I64, + quote_break_even_amount: -100 * QUOTE_PRECISION_I64, + ..PerpPosition::default() + }; + + let mut liquidator = User { + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let liquidator_key = Pubkey::default(); + + let mut user_stats = UserStats::default(); + let mut liquidator_stats = UserStats::default(); + let state = State { + liquidation_margin_buffer_ratio: 10, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + + let result = user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginRequirementType::Initial, + 1, + 0, + &mut user_stats, + now, + ); + + assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); + + let result: Result = user + .meets_withdraw_margin_requirement_and_increment_fuel_bonus_swap( + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginRequirementType::Initial, + 0, + 0, + 0, + 0, + &mut user_stats, + now, + ); + + assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); + } +} + mod update_open_bids_and_asks { use crate::state::user::{ Order, OrderBitFlag, OrderTriggerCondition, OrderType, PositionDirection, diff --git a/sdk/src/decode/user.ts b/sdk/src/decode/user.ts index e0d852f6e8..706a0698ba 100644 --- a/sdk/src/decode/user.ts +++ b/sdk/src/decode/user.ts @@ -84,6 +84,8 @@ export function decodeUser(buffer: Buffer): UserAccount { const quoteAssetAmount = readSignedBigInt64LE(buffer, offset + 16); const lpShares = readUnsignedBigInt64LE(buffer, offset + 64); const openOrders = buffer.readUInt8(offset + 94); + const positionFlag = buffer.readUInt8(offset + 95); + const isolatedPositionScaledBalance = readUnsignedBigInt64LE(buffer, offset + 96); if ( baseAssetAmount.eq(ZERO) && @@ -117,7 +119,6 @@ export function decodeUser(buffer: Buffer): UserAccount { offset += 3; const perLpBase = buffer.readUInt8(offset); offset += 1; - perpPositions.push({ lastCumulativeFundingRate, baseAssetAmount, @@ -135,7 +136,9 @@ export function decodeUser(buffer: Buffer): UserAccount { openOrders, perLpBase, maxMarginRatio, - }); + positionFlag, + isolatedPositionScaledBalance, + }); } const orders: Order[] = []; diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 0a86382d5b..d39f70dcd9 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -46,6 +46,7 @@ import { PhoenixV1FulfillmentConfigAccount, PlaceAndTakeOrderSuccessCondition, PositionDirection, + PositionFlag, ReferrerInfo, ReferrerNameAccount, SerumV3FulfillmentConfigAccount, @@ -57,7 +58,6 @@ import { StateAccount, SwapReduceOnly, SignedMsgOrderParamsMessage, - TakerInfo, TxParams, UserAccount, UserStatsAccount, @@ -194,6 +194,8 @@ import nacl from 'tweetnacl'; import { Slothash } from './slot/SlothashSubscriber'; import { getOracleId } from './oracles/oracleId'; import { SignedMsgOrderParams } from './types'; +import { TakerInfo } from './types'; +// BN is already imported globally in this file via other imports import { sha256 } from '@noble/hashes/sha256'; import { getOracleConfidenceFromMMOracleData } from './oracles/utils'; import { Commitment } from 'gill'; @@ -272,6 +274,46 @@ export class DriftClient { return this._isSubscribed && this.accountSubscriber.isSubscribed; } + private async getPrePlaceOrderIxs( + orderParams: OptionalOrderParams, + userAccount: UserAccount, + options?: { positionMaxLev?: number; isolatedPositionDepositAmount?: BN } + ): Promise { + const preIxs: TransactionInstruction[] = []; + + if (isVariant(orderParams.marketType, 'perp')) { + const { positionMaxLev, isolatedPositionDepositAmount } = options ?? {}; + + if ( + isolatedPositionDepositAmount?.gt?.(ZERO) && + this.isOrderIncreasingPosition(orderParams, userAccount) + ) { + preIxs.push( + await this.getTransferIsolatedPerpPositionDepositIx( + isolatedPositionDepositAmount as BN, + orderParams.marketIndex, + userAccount.subAccountId + ) + ); + } + + if (positionMaxLev) { + const marginRatio = Math.floor( + (1 / positionMaxLev) * MARGIN_PRECISION.toNumber() + ); + preIxs.push( + await this.getUpdateUserPerpPositionCustomMarginRatioIx( + orderParams.marketIndex, + marginRatio, + userAccount.subAccountId + ) + ); + } + } + + return preIxs; + } + public set isSubscribed(val: boolean) { this._isSubscribed = val; } @@ -745,7 +787,6 @@ export class DriftClient { return lookupTableAccount; } - public async fetchAllLookupTableAccounts(): Promise< AddressLookupTableAccount[] > { @@ -1712,7 +1753,6 @@ export class DriftClient { const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } - public async getUpdateUserCustomMarginRatioIx( marginRatio: number, subAccountId = 0 @@ -2426,6 +2466,15 @@ export class DriftClient { return this.getTokenAmount(QUOTE_SPOT_MARKET_INDEX); } + public getIsolatedPerpPositionTokenAmount( + perpMarketIndex: number, + subAccountId?: number + ): BN { + return this.getUser(subAccountId).getIsolatePerpPositionTokenAmount( + perpMarketIndex + ); + } + /** * Returns the token amount for a given market. The spot market precision is based on the token mint decimals. * Positive if it is a deposit, negative if it is a borrow. @@ -2503,7 +2552,6 @@ export class DriftClient { this.mustIncludeSpotMarketIndexes.add(spotMarketIndex); }); } - getRemainingAccounts(params: RemainingAccountParams): AccountMeta[] { const { oracleAccountMap, spotMarketAccountMap, perpMarketAccountMap } = this.getRemainingAccountMapsForUsers(params.userAccounts); @@ -3366,7 +3414,6 @@ export class DriftClient { userAccountPublicKey, }; } - public async createInitializeUserAccountAndDepositCollateral( amount: BN, userTokenAccount: PublicKey, @@ -4023,6 +4070,201 @@ export class DriftClient { ); } + async depositIntoIsolatedPerpPosition( + amount: BN, + perpMarketIndex: number, + userTokenAccount: PublicKey, + subAccountId?: number, + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getDepositIntoIsolatedPerpPositionIx( + amount, + perpMarketIndex, + userTokenAccount, + subAccountId + ), + txParams + ), + [], + this.opts + ); + return txSig; + } + + async getDepositIntoIsolatedPerpPositionIx( + amount: BN, + perpMarketIndex: number, + userTokenAccount: PublicKey, + subAccountId?: number + ): Promise { + const userAccountPublicKey = await getUserAccountPublicKey( + this.program.programId, + this.authority, + subAccountId ?? this.activeSubAccountId + ); + + const perpMarketAccount = this.getPerpMarketAccount(perpMarketIndex); + const spotMarketIndex = perpMarketAccount.quoteSpotMarketIndex; + const spotMarketAccount = this.getSpotMarketAccount(spotMarketIndex); + + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + writableSpotMarketIndexes: [spotMarketIndex], + readablePerpMarketIndex: [perpMarketIndex], + }); + + const tokenProgram = this.getTokenProgramForSpotMarket(spotMarketAccount); + return await this.program.instruction.depositIntoIsolatedPerpPosition( + spotMarketIndex, + perpMarketIndex, + amount, + { + accounts: { + state: await this.getStatePublicKey(), + spotMarketVault: spotMarketAccount.vault, + user: userAccountPublicKey, + userStats: this.getUserStatsAccountPublicKey(), + userTokenAccount: userTokenAccount, + authority: this.wallet.publicKey, + tokenProgram, + }, + remainingAccounts, + } + ); + } + + public async transferIsolatedPerpPositionDeposit( + amount: BN, + perpMarketIndex: number, + subAccountId?: number, + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getTransferIsolatedPerpPositionDepositIx( + amount, + perpMarketIndex, + subAccountId + ), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getTransferIsolatedPerpPositionDepositIx( + amount: BN, + perpMarketIndex: number, + subAccountId?: number + ): Promise { + const userAccountPublicKey = await getUserAccountPublicKey( + this.program.programId, + this.authority, + subAccountId ?? this.activeSubAccountId + ); + + const perpMarketAccount = this.getPerpMarketAccount(perpMarketIndex); + const spotMarketIndex = perpMarketAccount.quoteSpotMarketIndex; + const spotMarketAccount = this.getSpotMarketAccount(spotMarketIndex); + const user = await this.getUserAccount(subAccountId); + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [user], + writableSpotMarketIndexes: [spotMarketIndex], + readablePerpMarketIndex: [perpMarketIndex], + }); + + return await this.program.instruction.transferIsolatedPerpPositionDeposit( + spotMarketIndex, + perpMarketIndex, + amount, + { + accounts: { + state: await this.getStatePublicKey(), + spotMarketVault: spotMarketAccount.vault, + user: userAccountPublicKey, + userStats: this.getUserStatsAccountPublicKey(), + authority: this.wallet.publicKey, + }, + remainingAccounts, + } + ); + } + + public async withdrawFromIsolatedPerpPosition( + amount: BN, + perpMarketIndex: number, + userTokenAccount: PublicKey, + subAccountId?: number, + txParams?: TxParams + ): Promise { + const userAccountPublicKey = await getUserAccountPublicKey( + this.program.programId, + this.authority, + subAccountId ?? this.activeSubAccountId + ); + const userAccount = this.getUserAccount(subAccountId); + const settleIx = await this.settleMultiplePNLsIx( + userAccountPublicKey, + userAccount, + [perpMarketIndex], + SettlePnlMode.TRY_SETTLE + ); + const withdrawIx = await this.getWithdrawFromIsolatedPerpPositionIx( + amount, + perpMarketIndex, + userTokenAccount, + subAccountId + ); + const { txSig } = await this.sendTransaction( + await this.buildTransaction([settleIx, withdrawIx], txParams) + ); + return txSig; + } + + public async getWithdrawFromIsolatedPerpPositionIx( + amount: BN, + perpMarketIndex: number, + userTokenAccount: PublicKey, + subAccountId?: number + ): Promise { + const userAccountPublicKey = await getUserAccountPublicKey( + this.program.programId, + this.authority, + subAccountId ?? this.activeSubAccountId + ); + const perpMarketAccount = this.getPerpMarketAccount(perpMarketIndex); + const spotMarketIndex = perpMarketAccount.quoteSpotMarketIndex; + const spotMarketAccount = this.getSpotMarketAccount(spotMarketIndex); + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [this.getUserAccount(subAccountId)], + writableSpotMarketIndexes: [spotMarketIndex], + readablePerpMarketIndex: [perpMarketIndex], + }); + + return await this.program.instruction.withdrawFromIsolatedPerpPosition( + spotMarketIndex, + perpMarketIndex, + amount, + { + accounts: { + state: await this.getStatePublicKey(), + spotMarketVault: spotMarketAccount.vault, + user: userAccountPublicKey, + userStats: this.getUserStatsAccountPublicKey(), + authority: this.wallet.publicKey, + userTokenAccount: userTokenAccount, + tokenProgram: this.getTokenProgramForSpotMarket(spotMarketAccount), + driftSigner: this.getSignerPublicKey(), + }, + remainingAccounts, + } + ); + } + public async updateSpotMarketCumulativeInterest( marketIndex: number, txParams?: TxParams @@ -4166,7 +4408,6 @@ export class DriftClient { } ); } - public async getRemovePerpLpSharesIx( marketIndex: number, sharesToBurn?: BN, @@ -4323,7 +4564,8 @@ export class DriftClient { referrerInfo?: ReferrerInfo, cancelExistingOrders?: boolean, settlePnl?: boolean, - positionMaxLev?: number + positionMaxLev?: number, + isolatedPositionDepositAmount?: BN ): Promise<{ cancelExistingOrdersTx?: Transaction | VersionedTransaction; settlePnlTx?: Transaction | VersionedTransaction; @@ -4351,18 +4593,25 @@ export class DriftClient { const txKeys = Object.keys(ixPromisesForTxs); - const marketOrderTxIxs = positionMaxLev - ? this.getPlaceOrdersAndSetPositionMaxLevIx( - [orderParams, ...bracketOrdersParams], - positionMaxLev, - userAccount.subAccountId - ) - : this.getPlaceOrdersIx( - [orderParams, ...bracketOrdersParams], - userAccount.subAccountId - ); + const preIxs: TransactionInstruction[] = await this.getPrePlaceOrderIxs( + orderParams, + userAccount, + { + positionMaxLev, + isolatedPositionDepositAmount, + } + ); - ixPromisesForTxs.marketOrderTx = marketOrderTxIxs; + ixPromisesForTxs.marketOrderTx = (async () => { + const placeOrdersIx = await this.getPlaceOrdersIx( + [orderParams, ...bracketOrdersParams], + userAccount.subAccountId + ); + if (preIxs.length) { + return [...preIxs, placeOrdersIx] as unknown as TransactionInstruction; + } + return placeOrdersIx; + })(); /* Cancel open orders in market if requested */ if (cancelExistingOrders && isVariant(orderParams.marketType, 'perp')) { @@ -4480,12 +4729,29 @@ export class DriftClient { public async placePerpOrder( orderParams: OptionalOrderParams, txParams?: TxParams, - subAccountId?: number + subAccountId?: number, + isolatedPositionDepositAmount?: BN ): Promise { + const preIxs: TransactionInstruction[] = []; + if (isolatedPositionDepositAmount?.gt?.(ZERO)) { + preIxs.push( + await this.getTransferIsolatedPerpPositionDepositIx( + isolatedPositionDepositAmount as BN, + orderParams.marketIndex, + subAccountId + ) + ); + } + const { txSig, slot } = await this.sendTransaction( await this.buildTransaction( await this.getPlacePerpOrderIx(orderParams, subAccountId), - txParams + txParams, + undefined, + undefined, + undefined, + undefined, + preIxs ), [], this.opts @@ -4913,7 +5179,8 @@ export class DriftClient { params: OrderParams[], txParams?: TxParams, subAccountId?: number, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + isolatedPositionDepositAmount?: BN ): Promise { const { txSig } = await this.sendTransaction( ( @@ -4921,7 +5188,8 @@ export class DriftClient { params, txParams, subAccountId, - optionalIxs + optionalIxs, + isolatedPositionDepositAmount ) ).placeOrdersTx, [], @@ -4935,10 +5203,28 @@ export class DriftClient { params: OrderParams[], txParams?: TxParams, subAccountId?: number, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + isolatedPositionDepositAmount?: BN ) { const lookupTableAccounts = await this.fetchAllLookupTableAccounts(); + const preIxs: TransactionInstruction[] = []; + if (params?.length === 1) { + const p = params[0]; + if ( + isVariant(p.marketType, 'perp') && + isolatedPositionDepositAmount?.gt?.(ZERO) + ) { + preIxs.push( + await this.getTransferIsolatedPerpPositionDepositIx( + isolatedPositionDepositAmount as BN, + p.marketIndex, + subAccountId + ) + ); + } + } + const tx = await this.buildTransaction( await this.getPlaceOrdersIx(params, subAccountId), txParams, @@ -4946,14 +5232,13 @@ export class DriftClient { lookupTableAccounts, undefined, undefined, - optionalIxs + [...preIxs, ...(optionalIxs ?? [])] ); return { placeOrdersTx: tx, }; } - public async getPlaceOrdersIx( params: OptionalOrderParams[], subAccountId?: number, @@ -5062,8 +5347,7 @@ export class DriftClient { const marginRatio = Math.floor( (1 / positionMaxLev) * MARGIN_PRECISION.toNumber() ); - - // TODO: Handle multiple markets? + // Keep existing behavior but note: prefer using getPostPlaceOrderIxs path const setPositionMaxLevIxs = await this.getUpdateUserPerpPositionCustomMarginRatioIx( readablePerpMarketIndex[0], @@ -5795,7 +6079,6 @@ export class DriftClient { return txSig; } - public async getJupiterSwapIxV6({ jupiterClient, outMarketIndex, @@ -6465,7 +6748,6 @@ export class DriftClient { this.perpMarketLastSlotCache.set(orderParams.marketIndex, slot); return txSig; } - public async preparePlaceAndTakePerpOrderWithAdditionalOrders( orderParams: OptionalOrderParams, makerInfo?: MakerInfo | MakerInfo[], @@ -6477,7 +6759,8 @@ export class DriftClient { settlePnl?: boolean, exitEarlyIfSimFails?: boolean, auctionDurationPercentage?: number, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + isolatedPositionDepositAmount?: BN ): Promise<{ placeAndTakeTx: Transaction | VersionedTransaction; cancelExistingOrdersTx: Transaction | VersionedTransaction; @@ -6511,6 +6794,19 @@ export class DriftClient { subAccountId ); + if ( + isVariant(orderParams.marketType, 'perp') && + isolatedPositionDepositAmount?.gt?.(ZERO) + ) { + placeAndTakeIxs.push( + await this.getTransferIsolatedPerpPositionDepositIx( + isolatedPositionDepositAmount as BN, + orderParams.marketIndex, + subAccountId + ) + ); + } + placeAndTakeIxs.push(placeAndTakeIx); if (bracketOrdersParams.length > 0) { @@ -6521,6 +6817,11 @@ export class DriftClient { placeAndTakeIxs.push(bracketOrdersIx); } + // Optional extra ixs can be appended at the front + if (optionalIxs?.length) { + placeAndTakeIxs.unshift(...optionalIxs); + } + const shouldUseSimulationComputeUnits = txParams?.useSimulatedComputeUnits; const shouldExitIfSimulationFails = exitEarlyIfSimFails; @@ -7317,7 +7618,6 @@ export class DriftClient { this.spotMarketLastSlotCache.set(QUOTE_SPOT_MARKET_INDEX, slot); return txSig; } - public async getPlaceAndTakeSpotOrderIx( orderParams: OptionalOrderParams, fulfillmentConfig?: SerumV3FulfillmentConfigAccount, @@ -7780,7 +8080,6 @@ export class DriftClient { bitFlags?: number; policy?: ModifyOrderPolicy; maxTs?: BN; - txParams?: TxParams; }, subAccountId?: number ): Promise { @@ -8306,7 +8605,6 @@ export class DriftClient { this.perpMarketLastSlotCache.set(marketIndex, slot); return txSig; } - public async getLiquidatePerpIx( userAccountPublicKey: PublicKey, userAccount: UserAccount, @@ -9097,7 +9395,6 @@ export class DriftClient { } ); } - public async resolveSpotBankruptcy( userAccountPublicKey: PublicKey, userAccount: UserAccount, @@ -9929,7 +10226,6 @@ export class DriftClient { const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } - public async getSettleRevenueToInsuranceFundIx( spotMarketIndex: number ): Promise { @@ -10724,7 +11020,6 @@ export class DriftClient { ); return config as ProtectedMakerModeConfig; } - public async updateUserProtectedMakerOrders( subAccountId: number, protectedOrders: boolean, @@ -11048,4 +11343,22 @@ export class DriftClient { forceVersionedTransaction, }); } + + isOrderIncreasingPosition( + orderParams: OptionalOrderParams, + userAccount: UserAccount + ): boolean { + const perpPosition = userAccount.perpPositions.find( + (p) => p.marketIndex === orderParams.marketIndex + ); + if (!perpPosition) return true; + + const currentBase = perpPosition.baseAssetAmount; + if (currentBase.eq(ZERO)) return true; + + return currentBase + .add(orderParams.baseAssetAmount) + .abs() + .gt(currentBase.abs()); + } } diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index eed45293ca..60441df3d2 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -652,6 +652,163 @@ } ] }, + { + "name": "depositIntoIsolatedPerpPosition", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "userTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "transferIsolatedPerpPositionDeposit", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "i64" + } + ] + }, + { + "name": "withdrawFromIsolatedPerpPosition", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "userTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "u64" + } + ] + }, { "name": "placePerpOrder", "accounts": [ @@ -11863,13 +12020,13 @@ "type": "u64" }, { - "name": "lastBaseAssetAmountPerLp", + "name": "isolatedPositionScaledBalance", "docs": [ "The last base asset amount per lp the amm had", "Used to settle the users lp position", - "precision: BASE_PRECISION" + "precision: SPOT_BALANCE_PRECISION" ], - "type": "i64" + "type": "u64" }, { "name": "lastQuoteAssetAmountPerLp", @@ -11908,8 +12065,8 @@ "type": "u8" }, { - "name": "perLpBase", - "type": "i8" + "name": "positionFlag", + "type": "u8" } ] } @@ -12547,6 +12704,17 @@ ] } }, + { + "name": "LiquidationBitFlag", + "type": { + "kind": "enum", + "variants": [ + { + "name": "IsolatedPosition" + } + ] + } + }, { "name": "SettlePnlExplanation", "type": { @@ -12662,13 +12830,7 @@ "kind": "enum", "variants": [ { - "name": "Standard", - "fields": [ - { - "name": "trackOpenOrdersFraction", - "type": "bool" - } - ] + "name": "Standard" }, { "name": "Liquidation", @@ -13259,6 +13421,23 @@ ] } }, + { + "name": "PositionFlag", + "type": { + "kind": "enum", + "variants": [ + { + "name": "IsolatedPosition" + }, + { + "name": "BeingLiquidated" + }, + { + "name": "Bankrupt" + } + ] + } + }, { "name": "ReferrerStatus", "type": { @@ -14223,6 +14402,11 @@ "defined": "SpotBankruptcyRecord" }, "index": false + }, + { + "name": "bitFlags", + "type": "u8", + "index": false } ] }, @@ -15659,8 +15843,8 @@ }, { "code": 6094, - "name": "CantUpdatePoolBalanceType", - "msg": "CantUpdatePoolBalanceType" + "name": "CantUpdateSpotBalanceType", + "msg": "CantUpdateSpotBalanceType" }, { "code": 6095, @@ -16772,6 +16956,11 @@ "name": "InvalidIfRebalanceSwap", "msg": "Invalid If Rebalance Swap" }, + { + "code": 6317, + "name": "InvalidIsolatedPerpMarket", + "msg": "Invalid Isolated Perp Market" + }, { "code": 6317, "name": "InvalidRevenueShareResize", diff --git a/sdk/src/margin/README.md b/sdk/src/margin/README.md new file mode 100644 index 0000000000..b074e96ff2 --- /dev/null +++ b/sdk/src/margin/README.md @@ -0,0 +1,143 @@ +## Margin Calculation Snapshot (SDK) + +This document describes the single-source-of-truth margin engine in the SDK that mirrors the on-chain `MarginCalculation` and related semantics. The goal is to compute an immutable snapshot in one pass and have existing `User` getters delegate to it, eliminating duplicative work across getters and UI hooks while maintaining parity with the program. + +### Alignment with on-chain + +- The SDK snapshot shape mirrors `programs/drift/src/state/margin_calculation.rs` field-for-field. +- The inputs and ordering mirror `calculate_margin_requirement_and_total_collateral_and_liability_info` in `programs/drift/src/math/margin.rs`. +- Isolated positions are represented as `isolated_margin_calculations` keyed by perp `market_index`, matching program logic. + +### Core SDK types (shape parity) + +```ts +// Types reflect on-chain names and numeric signs +export type MarginRequirementType = 'Initial' | 'Fill' | 'Maintenance'; +export type MarketType = 'Spot' | 'Perp'; + +export type MarketIdentifier = { + marketType: MarketType; + marketIndex: number; // u16 +}; + +export type MarginCalculationMode = + | { kind: 'Standard' } + | { kind: 'Liquidation'; marketToTrackMarginRequirement?: MarketIdentifier }; + +export type MarginContext = { + marginType: MarginRequirementType; + mode: MarginCalculationMode; + strict: boolean; + ignoreInvalidDepositOracles: boolean; + marginBuffer: BN; // u128 + fuelBonusNumerator: number; // i64 + fuelBonus: number; // u64 + fuelPerpDelta?: { marketIndex: number; delta: BN }; // (u16, i64) + fuelSpotDeltas: Array<{ marketIndex: number; delta: BN }>; // up to 2 entries + marginRatioOverride?: number; // u32 +}; + +export type IsolatedMarginCalculation = { + marginRequirement: BN; // u128 + totalCollateral: BN; // i128 + totalCollateralBuffer: BN; // i128 + marginRequirementPlusBuffer: BN; // u128 +}; + +export type MarginCalculation = { + context: MarginContext; + + totalCollateral: BN; // i128 + totalCollateralBuffer: BN; // i128 + marginRequirement: BN; // u128 + marginRequirementPlusBuffer: BN; // u128 + + isolatedMarginCalculations: Map; // BTreeMap + + numSpotLiabilities: number; // u8 + numPerpLiabilities: number; // u8 + allDepositOraclesValid: boolean; + allLiabilityOraclesValid: boolean; + withPerpIsolatedLiability: boolean; + withSpotIsolatedLiability: boolean; + + totalSpotAssetValue: BN; // i128 + totalSpotLiabilityValue: BN; // u128 + totalPerpLiabilityValue: BN; // u128 + totalPerpPnl: BN; // i128 + + trackedMarketMarginRequirement: BN; // u128 + fuelDeposits: number; // u32 + fuelBorrows: number; // u32 + fuelPositions: number; // u32 +}; +``` + +### Engine API + +```ts +// Pure computation, no I/O; uses data already cached in the client/subscribers +export function computeMarginCalculation(user: User, context: MarginContext): MarginCalculation; + +// Helpers that mirror on-chain semantics +export function meets_margin_requirement(calc: MarginCalculation): boolean; +export function meets_margin_requirement_with_buffer(calc: MarginCalculation): boolean; +export function get_cross_free_collateral(calc: MarginCalculation): BN; +export function get_isolated_free_collateral(calc: MarginCalculation, marketIndex: number): BN; +export function cross_margin_shortage(calc: MarginCalculation): BN; // requires buffer mode +export function isolated_margin_shortage(calc: MarginCalculation, marketIndex: number): BN; // requires buffer mode +``` + +### Computation model (on-demand) + +- The SDK computes the snapshot on-demand when `getMarginCalculation(...)` is called. +- No event-driven recomputation by default (oracle prices can change every slot; recomputing every update would be wasteful). +- Callers (UI/bots) decide polling frequency (e.g., UI can refresh every ~1s on active trade forms). + +### User integration + +- Add `user.getMarginCalculation(margin_type = 'Initial', overrides?: Partial)`. +- Existing getters delegate to the snapshot to avoid duplicate work: + - `getTotalCollateral()` → `snapshot.total_collateral` + - `getMarginRequirement(mode)` → `snapshot.margin_requirement` + - `getFreeCollateral()` → `get_cross_free_collateral(snapshot)` + - Per-market isolated FC → `get_isolated_free_collateral(snapshot, marketIndex)` + +Suggested `User` API surface (non-breaking): + +```ts +// Primary entrypoint +getMarginCalculation( + marginType: 'Initial' | 'Maintenance' | 'Fill' = 'Initial', + contextOverrides?: Partial +): MarginCalculation; + +// Optional conveniences for consumers +getIsolatedMarginCalculation( + marketIndex: number, + marginType: 'Initial' | 'Maintenance' | 'Fill' = 'Initial', + contextOverrides?: Partial +): IsolatedMarginCalculation | undefined; + +// Cross views can continue to use helpers on the snapshot: +// get_cross_free_collateral(snapshot), meets_margin_requirement(snapshot), etc. +``` + +### UI compatibility + +- All existing `User` getters remain and delegate to the snapshot, so current UI keeps working without call-site changes. +- New consumers can call `user.getMarginCalculation()` to access isolated breakdowns. + +### Testing and parity + +- Golden tests comparing SDK snapshot against program outputs (cross and isolated, edge cases). +- Keep math/rounding identical to program (ordering, buffers, funding, open-order IM, oracle strictness). + +### Migration plan (brief) + +1. Implement `types` and `engine` with strict parity; land behind a feature flag. +2. Add `user.getMarginCalculation()` and delegate legacy getters. +3. Optionally update UI hooks to read richer fields; not required for compatibility. +4. Expand parity tests; enable by default after validation. + + diff --git a/sdk/src/marginCalculation.ts b/sdk/src/marginCalculation.ts new file mode 100644 index 0000000000..fe9fa8d7eb --- /dev/null +++ b/sdk/src/marginCalculation.ts @@ -0,0 +1,315 @@ +import { BN } from '@coral-xyz/anchor'; +import { MARGIN_PRECISION } from './constants/numericConstants'; +import { MarketType } from './types'; + +export type MarginCategory = 'Initial' | 'Maintenance' | 'Fill'; + +export type MarginCalculationMode = + | { type: 'Standard' } + | { type: 'Liquidation' }; + +export class MarketIdentifier { + marketType: MarketType; + marketIndex: number; + + private constructor(marketType: MarketType, marketIndex: number) { + this.marketType = marketType; + this.marketIndex = marketIndex; + } + + static spot(marketIndex: number): MarketIdentifier { + return new MarketIdentifier(MarketType.SPOT, marketIndex); + } + + static perp(marketIndex: number): MarketIdentifier { + return new MarketIdentifier(MarketType.PERP, marketIndex); + } + + equals(other: MarketIdentifier | undefined): boolean { + return ( + !!other && + this.marketType === other.marketType && + this.marketIndex === other.marketIndex + ); + } +} + +export class MarginContext { + marginType: MarginCategory; + mode: MarginCalculationMode; + strict: boolean; + ignoreInvalidDepositOracles: boolean; + marginBuffer: BN; // scaled by MARGIN_PRECISION + marginRatioOverride?: number; + + private constructor(marginType: MarginCategory) { + this.marginType = marginType; + this.mode = { type: 'Standard' }; + this.strict = false; + this.ignoreInvalidDepositOracles = false; + this.marginBuffer = new BN(0); + } + + static standard(marginType: MarginCategory): MarginContext { + return new MarginContext(marginType); + } + + static liquidation(marginBuffer: BN): MarginContext { + const ctx = new MarginContext('Maintenance'); + ctx.mode = { type: 'Liquidation' }; + ctx.marginBuffer = marginBuffer ?? new BN(0); + return ctx; + } + + strictMode(strict: boolean): this { + this.strict = strict; + return this; + } + + ignoreInvalidDeposits(ignore: boolean): this { + this.ignoreInvalidDepositOracles = ignore; + return this; + } + + setMarginBuffer(buffer?: BN): this { + this.marginBuffer = buffer ?? new BN(0); + return this; + } + + setMarginRatioOverride(ratio: number): this { + this.marginRatioOverride = ratio; + return this; + } + + trackMarketMarginRequirement(marketIdentifier: MarketIdentifier): this { + if (this.mode.type !== 'Liquidation') { + throw new Error( + 'InvalidMarginCalculation: Cant track market outside of liquidation mode' + ); + } + return this; + } +} + +export class IsolatedMarginCalculation { + marginRequirement: BN; + totalCollateral: BN; // deposit + pnl + totalCollateralBuffer: BN; + marginRequirementPlusBuffer: BN; + + constructor() { + this.marginRequirement = new BN(0); + this.totalCollateral = new BN(0); + this.totalCollateralBuffer = new BN(0); + this.marginRequirementPlusBuffer = new BN(0); + } + + getTotalCollateralPlusBuffer(): BN { + return this.totalCollateral.add(this.totalCollateralBuffer); + } + + meetsMarginRequirement(): boolean { + return this.totalCollateral.gte(this.marginRequirement); + } + + meetsMarginRequirementWithBuffer(): boolean { + return this.getTotalCollateralPlusBuffer().gte( + this.marginRequirementPlusBuffer + ); + } + + marginShortage(): BN { + const shortage = this.marginRequirementPlusBuffer.sub( + this.getTotalCollateralPlusBuffer() + ); + return shortage.isNeg() ? new BN(0) : shortage; + } +} + +export class MarginCalculation { + context: MarginContext; + totalCollateral: BN; + totalCollateralBuffer: BN; + marginRequirement: BN; + marginRequirementPlusBuffer: BN; + isolatedMarginCalculations: Map; + numSpotLiabilities: number; + numPerpLiabilities: number; + allDepositOraclesValid: boolean; + allLiabilityOraclesValid: boolean; + withPerpIsolatedLiability: boolean; + withSpotIsolatedLiability: boolean; + totalSpotLiabilityValue: BN; + totalPerpLiabilityValue: BN; + trackedMarketMarginRequirement: BN; + fuelDeposits: number; + fuelBorrows: number; + fuelPositions: number; + + constructor(context: MarginContext) { + this.context = context; + this.totalCollateral = new BN(0); + this.totalCollateralBuffer = new BN(0); + this.marginRequirement = new BN(0); + this.marginRequirementPlusBuffer = new BN(0); + this.isolatedMarginCalculations = new Map(); + this.numSpotLiabilities = 0; + this.numPerpLiabilities = 0; + this.allDepositOraclesValid = true; + this.allLiabilityOraclesValid = true; + this.withPerpIsolatedLiability = false; + this.withSpotIsolatedLiability = false; + this.totalSpotLiabilityValue = new BN(0); + this.totalPerpLiabilityValue = new BN(0); + this.trackedMarketMarginRequirement = new BN(0); + this.fuelDeposits = 0; + this.fuelBorrows = 0; + this.fuelPositions = 0; + } + + addCrossMarginTotalCollateral(delta: BN): void { + this.totalCollateral = this.totalCollateral.add(delta); + if (this.context.marginBuffer.gt(new BN(0)) && delta.isNeg()) { + this.totalCollateralBuffer = this.totalCollateralBuffer.add( + delta.mul(this.context.marginBuffer).div(MARGIN_PRECISION) + ); + } + } + + addCrossMarginRequirement(marginRequirement: BN, liabilityValue: BN): void { + this.marginRequirement = this.marginRequirement.add(marginRequirement); + if (this.context.marginBuffer.gt(new BN(0))) { + this.marginRequirementPlusBuffer = this.marginRequirementPlusBuffer.add( + marginRequirement.add( + liabilityValue.mul(this.context.marginBuffer).div(MARGIN_PRECISION) + ) + ); + } + } + + addIsolatedMarginCalculation( + marketIndex: number, + depositValue: BN, + pnl: BN, + liabilityValue: BN, + marginRequirement: BN + ): void { + const totalCollateral = depositValue.add(pnl); + const totalCollateralBuffer = + this.context.marginBuffer.gt(new BN(0)) && pnl.isNeg() + ? pnl.mul(this.context.marginBuffer).div(MARGIN_PRECISION) + : new BN(0); + + const marginRequirementPlusBuffer = this.context.marginBuffer.gt(new BN(0)) + ? marginRequirement.add( + liabilityValue.mul(this.context.marginBuffer).div(MARGIN_PRECISION) + ) + : new BN(0); + + const iso = new IsolatedMarginCalculation(); + iso.marginRequirement = marginRequirement; + iso.totalCollateral = totalCollateral; + iso.totalCollateralBuffer = totalCollateralBuffer; + iso.marginRequirementPlusBuffer = marginRequirementPlusBuffer; + this.isolatedMarginCalculations.set(marketIndex, iso); + } + + addSpotLiability(): void { + this.numSpotLiabilities += 1; + } + + addPerpLiability(): void { + this.numPerpLiabilities += 1; + } + + addSpotLiabilityValue(spotLiabilityValue: BN): void { + this.totalSpotLiabilityValue = + this.totalSpotLiabilityValue.add(spotLiabilityValue); + } + + addPerpLiabilityValue(perpLiabilityValue: BN): void { + this.totalPerpLiabilityValue = + this.totalPerpLiabilityValue.add(perpLiabilityValue); + } + + updateAllDepositOraclesValid(valid: boolean): void { + this.allDepositOraclesValid = this.allDepositOraclesValid && valid; + } + + updateAllLiabilityOraclesValid(valid: boolean): void { + this.allLiabilityOraclesValid = this.allLiabilityOraclesValid && valid; + } + + updateWithSpotIsolatedLiability(isolated: boolean): void { + this.withSpotIsolatedLiability = this.withSpotIsolatedLiability || isolated; + } + + updateWithPerpIsolatedLiability(isolated: boolean): void { + this.withPerpIsolatedLiability = this.withPerpIsolatedLiability || isolated; + } + + validateNumSpotLiabilities(): void { + if (this.numSpotLiabilities > 0 && this.marginRequirement.eq(new BN(0))) { + throw new Error( + 'InvalidMarginRatio: num_spot_liabilities>0 but margin_requirement=0' + ); + } + } + + getNumOfLiabilities(): number { + return this.numSpotLiabilities + this.numPerpLiabilities; + } + + getCrossTotalCollateralPlusBuffer(): BN { + return this.totalCollateral.add(this.totalCollateralBuffer); + } + + meetsCrossMarginRequirement(): boolean { + return this.totalCollateral.gte(this.marginRequirement); + } + + meetsCrossMarginRequirementWithBuffer(): boolean { + return this.getCrossTotalCollateralPlusBuffer().gte( + this.marginRequirementPlusBuffer + ); + } + + meetsMarginRequirement(): boolean { + if (!this.meetsCrossMarginRequirement()) return false; + for (const [, iso] of this.isolatedMarginCalculations) { + if (!iso.meetsMarginRequirement()) return false; + } + return true; + } + + meetsMarginRequirementWithBuffer(): boolean { + if (!this.meetsCrossMarginRequirementWithBuffer()) return false; + for (const [, iso] of this.isolatedMarginCalculations) { + if (!iso.meetsMarginRequirementWithBuffer()) return false; + } + return true; + } + + getCrossFreeCollateral(): BN { + const free = this.totalCollateral.sub(this.marginRequirement); + return free.isNeg() ? new BN(0) : free; + } + + getIsolatedFreeCollateral(marketIndex: number): BN { + const iso = this.isolatedMarginCalculations.get(marketIndex); + if (!iso) + throw new Error('InvalidMarginCalculation: missing isolated calc'); + const free = iso.totalCollateral.sub(iso.marginRequirement); + return free.isNeg() ? new BN(0) : free; + } + + getIsolatedMarginCalculation( + marketIndex: number + ): IsolatedMarginCalculation | undefined { + return this.isolatedMarginCalculations.get(marketIndex); + } + + hasIsolatedMarginCalculation(marketIndex: number): boolean { + return this.isolatedMarginCalculations.has(marketIndex); + } +} diff --git a/sdk/src/math/margin.ts b/sdk/src/math/margin.ts index 63fada436b..d67a5e2b67 100644 --- a/sdk/src/math/margin.ts +++ b/sdk/src/math/margin.ts @@ -160,8 +160,20 @@ export function calculateWorstCaseBaseAssetAmount( export function calculateWorstCasePerpLiabilityValue( perpPosition: PerpPosition, perpMarket: PerpMarketAccount, - oraclePrice: BN + oraclePrice: BN, + includeOpenOrders: boolean = true ): { worstCaseBaseAssetAmount: BN; worstCaseLiabilityValue: BN } { + // return early if no open orders required + if (!includeOpenOrders) { + return { + worstCaseBaseAssetAmount: perpPosition.baseAssetAmount, + worstCaseLiabilityValue: calculatePerpLiabilityValue( + perpPosition.baseAssetAmount, + oraclePrice, + isVariant(perpMarket.contractType, 'prediction') + ), + }; + } const allBids = perpPosition.baseAssetAmount.add(perpPosition.openBids); const allAsks = perpPosition.baseAssetAmount.add(perpPosition.openAsks); diff --git a/sdk/src/math/position.ts b/sdk/src/math/position.ts index 3db5007a20..6a5da647ec 100644 --- a/sdk/src/math/position.ts +++ b/sdk/src/math/position.ts @@ -14,6 +14,7 @@ import { PositionDirection, PerpPosition, SpotMarketAccount, + PositionFlag, } from '../types'; import { calculateUpdatedAMM, @@ -127,7 +128,6 @@ export function calculatePositionPNL( if (withFunding) { const fundingRatePnL = calculateUnsettledFundingPnl(market, perpPosition); - pnl = pnl.add(fundingRatePnL); } @@ -244,10 +244,16 @@ export function positionIsAvailable(position: PerpPosition): boolean { position.baseAssetAmount.eq(ZERO) && position.openOrders === 0 && position.quoteAssetAmount.eq(ZERO) && - position.lpShares.eq(ZERO) + position.lpShares.eq(ZERO) && + position.isolatedPositionScaledBalance.eq(ZERO) + && !positionIsBeingLiquidated(position) ); } +export function positionIsBeingLiquidated(position: PerpPosition): boolean { + return (position.positionFlag & (PositionFlag.BeingLiquidated | PositionFlag.Bankruptcy)) > 0; +} + /** * * @param userPosition diff --git a/sdk/src/math/spotPosition.ts b/sdk/src/math/spotPosition.ts index 61eae83ec1..ac5c6d7611 100644 --- a/sdk/src/math/spotPosition.ts +++ b/sdk/src/math/spotPosition.ts @@ -33,7 +33,8 @@ export function getWorstCaseTokenAmounts( spotMarketAccount: SpotMarketAccount, strictOraclePrice: StrictOraclePrice, marginCategory: MarginCategory, - customMarginRatio?: number + customMarginRatio?: number, + includeOpenOrders: boolean = true ): OrderFillSimulation { const tokenAmount = getSignedTokenAmount( getTokenAmount( @@ -50,7 +51,7 @@ export function getWorstCaseTokenAmounts( strictOraclePrice ); - if (spotPosition.openBids.eq(ZERO) && spotPosition.openAsks.eq(ZERO)) { + if (spotPosition.openBids.eq(ZERO) && spotPosition.openAsks.eq(ZERO) || !includeOpenOrders) { const { weight, weightedTokenValue } = calculateWeightedTokenValue( tokenAmount, tokenValue, diff --git a/sdk/src/types.ts b/sdk/src/types.ts index fd0cb165cd..dd4a9d8b8c 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -584,6 +584,10 @@ export type SpotBankruptcyRecord = { ifPayment: BN; }; +export class LiquidationBitFlag { + static readonly IsolatedPosition = 1; +} + export type SettlePnlRecord = { ts: BN; user: PublicKey; @@ -1134,6 +1138,8 @@ export type PerpPosition = { lastBaseAssetAmountPerLp: BN; lastQuoteAssetAmountPerLp: BN; perLpBase: number; + positionFlag: number; + isolatedPositionScaledBalance: BN; }; export type UserStatsAccount = { @@ -1286,6 +1292,12 @@ export class OrderParamsBitFlag { static readonly UpdateHighLeverageMode = 2; } +export class PositionFlag { + static readonly IsolatedPosition = 1; + static readonly BeingLiquidated = 2; + static readonly Bankruptcy = 3; +} + export type NecessaryOrderParams = { orderType: OrderType; marketIndex: number; @@ -1297,6 +1309,10 @@ export type OptionalOrderParams = { [Property in keyof OrderParams]?: OrderParams[Property]; } & NecessaryOrderParams; +export type PerpOrderIsolatedExtras = { + isolatedPositionDepositAmount?: BN; +}; + export type ModifyOrderParams = { [Property in keyof OrderParams]?: OrderParams[Property] | null; } & { policy?: ModifyOrderPolicy }; @@ -1334,6 +1350,7 @@ export type SignedMsgOrderParamsMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; + isolatedPositionDeposit?: BN | null; builderIdx?: number | null; builderFeeTenthBps?: number | null; }; @@ -1346,6 +1363,7 @@ export type SignedMsgOrderParamsDelegateMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; + isolatedPositionDeposit?: BN | null; builderIdx?: number | null; builderFeeTenthBps?: number | null; }; diff --git a/sdk/src/user.ts b/sdk/src/user.ts index a36820306d..e255b1a801 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -68,6 +68,7 @@ import { getUser30dRollingVolumeEstimate } from './math/trade'; import { MarketType, PositionDirection, + PositionFlag, SpotBalanceType, SpotMarketAccount, } from './types'; @@ -106,6 +107,12 @@ import { StrictOraclePrice } from './oracles/strictOraclePrice'; import { calculateSpotFuelBonus, calculatePerpFuelBonus } from './math/fuel'; import { grpcUserAccountSubscriber } from './accounts/grpcUserAccountSubscriber'; +import { + MarginCalculation, + MarginContext, +} from './marginCalculation'; + +export type MarginType = 'Cross' | 'Isolated'; export class User { driftClient: DriftClient; @@ -118,6 +125,282 @@ export class User { return this._isSubscribed && this.accountSubscriber.isSubscribed; } + /** + * Compute a consolidated margin snapshot once, without caching. + * Consumers can use this to avoid duplicating work across separate calls. + */ + // TODO: need another param to tell it give it back leverage compnents + // TODO: change get leverage functions need to pull the right values from + public getMarginCalculation( + marginCategory: MarginCategory = 'Initial', + opts?: { + strict?: boolean; // mirror StrictOraclePrice application + includeOpenOrders?: boolean; + enteringHighLeverage?: boolean; + liquidationBuffer?: BN; // margin_buffer analog for buffer mode + marginRatioOverride?: number; // mirrors context.margin_ratio_override + } + ): MarginCalculation { + const strict = opts?.strict ?? false; + const enteringHighLeverage = opts?.enteringHighLeverage ?? false; + const includeOpenOrders = opts?.includeOpenOrders ?? true; // TODO: remove this ?? + const marginBuffer = opts?.liquidationBuffer; // treat as MARGIN_BUFFER ratio if provided + const marginRatioOverride = opts?.marginRatioOverride; + + // Equivalent to on-chain user_custom_margin_ratio + let userCustomMarginRatio = + marginCategory === 'Initial' ? this.getUserAccount().maxMarginRatio : 0; + if (marginRatioOverride !== undefined) { + userCustomMarginRatio = Math.max( + userCustomMarginRatio, + marginRatioOverride + ); + } + + // Initialize calc via JS mirror of Rust MarginCalculation + const ctx = MarginContext.standard(marginCategory) + .strictMode(strict) + .setMarginBuffer(marginBuffer) + .setMarginRatioOverride(userCustomMarginRatio); + const calc = new MarginCalculation(ctx); + + // SPOT POSITIONS + // TODO: include open orders in the worst-case simulation in the same way on both spot and perp positions + for (const spotPosition of this.getUserAccount().spotPositions) { + if (isSpotPositionAvailable(spotPosition)) continue; + + const spotMarket = this.driftClient.getSpotMarketAccount( + spotPosition.marketIndex + ); + const oraclePriceData = this.getOracleDataForSpotMarket( + spotPosition.marketIndex + ); + const twap5 = strict + ? calculateLiveOracleTwap( + spotMarket.historicalOracleData, + oraclePriceData, + new BN(Math.floor(Date.now() / 1000)), + FIVE_MINUTE + ) + : undefined; + const strictOracle = new StrictOraclePrice(oraclePriceData.price, twap5); + + if (spotPosition.marketIndex === QUOTE_SPOT_MARKET_INDEX) { + const tokenAmount = getSignedTokenAmount( + getTokenAmount( + spotPosition.scaledBalance, + spotMarket, + spotPosition.balanceType + ), + spotPosition.balanceType + ); + if (isVariant(spotPosition.balanceType, 'deposit')) { + // add deposit value to total collateral + const tokenValue = getStrictTokenValue( + tokenAmount, + spotMarket.decimals, + strictOracle + ); + calc.addCrossMarginTotalCollateral(tokenValue); + } else { + // borrow on quote contributes to margin requirement + const tokenValueAbs = getStrictTokenValue( + tokenAmount, + spotMarket.decimals, + strictOracle + ).abs(); + calc.addCrossMarginRequirement(tokenValueAbs, tokenValueAbs); + calc.addSpotLiability(); + } + continue; + } + + // Non-quote spot: worst-case simulation + const { + tokenAmount: worstCaseTokenAmount, + ordersValue: worstCaseOrdersValue, + tokenValue: worstCaseTokenValue, + weightedTokenValue: worstCaseWeightedTokenValue, + } = getWorstCaseTokenAmounts( + spotPosition, + spotMarket, + strictOracle, + marginCategory, + userCustomMarginRatio, + includeOpenOrders + ); + + // open order IM + calc.addCrossMarginRequirement( + new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT), + ZERO + ); + + if (worstCaseTokenAmount.gt(ZERO)) { + // asset side increases total collateral (weighted) + calc.addCrossMarginTotalCollateral(worstCaseWeightedTokenValue); + } else if (worstCaseTokenAmount.lt(ZERO)) { + // liability side increases margin requirement (weighted >= abs(token_value)) + const liabilityWeighted = worstCaseWeightedTokenValue.abs(); + calc.addCrossMarginRequirement( + liabilityWeighted, + worstCaseTokenValue.abs() + ); + calc.addSpotLiability(); + calc.addSpotLiabilityValue(worstCaseTokenValue.abs()); + } else if (spotPosition.openOrders !== 0) { + calc.addSpotLiability(); + calc.addSpotLiabilityValue(worstCaseTokenValue.abs()); + } + + // orders value contributes to collateral or requirement + if (worstCaseOrdersValue.gt(ZERO)) { + calc.addCrossMarginTotalCollateral(worstCaseOrdersValue); + } else if (worstCaseOrdersValue.lt(ZERO)) { + const absVal = worstCaseOrdersValue.abs(); + calc.addCrossMarginRequirement(absVal, absVal); + } + } + + // PERP POSITIONS + for (const marketPosition of this.getActivePerpPositions()) { + const market = this.driftClient.getPerpMarketAccount( + marketPosition.marketIndex + ); + const quoteSpotMarket = this.driftClient.getSpotMarketAccount( + market.quoteSpotMarketIndex + ); + const quoteOraclePriceData = this.getOracleDataForSpotMarket( + market.quoteSpotMarketIndex + ); + const oraclePriceData = this.getOracleDataForPerpMarket( + market.marketIndex + ); + + // Worst-case perp liability and weighted pnl + const { worstCaseBaseAssetAmount, worstCaseLiabilityValue } = + calculateWorstCasePerpLiabilityValue( + marketPosition, + market, + oraclePriceData.price, + includeOpenOrders + ); + + // margin ratio for this perp + const customMarginRatio = Math.max(this.getUserAccount().maxMarginRatio, marketPosition.maxMarginRatio); + let marginRatio = new BN( + calculateMarketMarginRatio( + market, + worstCaseBaseAssetAmount.abs(), + marginCategory, + customMarginRatio, + this.isHighLeverageMode(marginCategory) || enteringHighLeverage + ) + ); + if (isVariant(market.status, 'settlement')) { + marginRatio = ZERO; + } + + // convert liability to quote value and apply margin ratio + const quotePrice = strict + ? BN.max( + quoteOraclePriceData.price, + quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + ) + : quoteOraclePriceData.price; + let perpMarginRequirement = worstCaseLiabilityValue + .mul(quotePrice) + .div(PRICE_PRECISION) + .mul(marginRatio) + .div(MARGIN_PRECISION); + // add open orders IM + perpMarginRequirement = perpMarginRequirement.add( + new BN(marketPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT) + ); + + // weighted unrealized pnl + let positionUnrealizedPnl = calculatePositionPNL( + market, + marketPosition, + true, + oraclePriceData + ); + let pnlQuotePrice: BN; + if (strict && positionUnrealizedPnl.gt(ZERO)) { + pnlQuotePrice = BN.min( + quoteOraclePriceData.price, + quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + ); + } else if (strict && positionUnrealizedPnl.lt(ZERO)) { + pnlQuotePrice = BN.max( + quoteOraclePriceData.price, + quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + ); + } else { + pnlQuotePrice = quoteOraclePriceData.price; + } + positionUnrealizedPnl = positionUnrealizedPnl + .mul(pnlQuotePrice) + .div(PRICE_PRECISION); + + // Add perp contribution: isolated vs cross + const isIsolated = this.isPerpPositionIsolated(marketPosition); + if (isIsolated) { + // derive isolated quote deposit value, mirroring on-chain logic + let depositValue = ZERO; + if (marketPosition.isolatedPositionScaledBalance.gt(ZERO)) { + const quoteSpotMarket = this.driftClient.getSpotMarketAccount( + market.quoteSpotMarketIndex + ); + const quoteOraclePriceData = this.getOracleDataForSpotMarket( + market.quoteSpotMarketIndex + ); + const strictQuote = new StrictOraclePrice( + quoteOraclePriceData.price, + strict + ? quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + : undefined + ); + const quoteTokenAmount = getTokenAmount( + marketPosition.isolatedPositionScaledBalance, + quoteSpotMarket, + SpotBalanceType.DEPOSIT + ); + depositValue = getStrictTokenValue( + quoteTokenAmount, + quoteSpotMarket.decimals, + strictQuote + ); + } + calc.addIsolatedMarginCalculation( + market.marketIndex, + depositValue, + positionUnrealizedPnl, + worstCaseLiabilityValue, + perpMarginRequirement + ); + calc.addPerpLiability(); + calc.addPerpLiabilityValue(worstCaseLiabilityValue); + } else { + // cross: add to global requirement and collateral + calc.addCrossMarginRequirement( + perpMarginRequirement, + worstCaseLiabilityValue + ); + calc.addCrossMarginTotalCollateral(positionUnrealizedPnl); + const hasPerpLiability = + !marketPosition.baseAssetAmount.eq(ZERO) || + marketPosition.quoteAssetAmount.lt(ZERO) || + marketPosition.openOrders !== 0; + if (hasPerpLiability) { + calc.addPerpLiability(); + } + } + } + + return calc; + } + public set isSubscribed(val: boolean) { this._isSubscribed = val; } @@ -331,9 +614,27 @@ export class User { lastQuoteAssetAmountPerLp: ZERO, perLpBase: 0, maxMarginRatio: 0, + positionFlag: 0, + isolatedPositionScaledBalance: ZERO, }; } + public getIsolatePerpPositionTokenAmount(perpMarketIndex: number): BN { + const perpPosition = this.getPerpPosition(perpMarketIndex); + const perpMarket = this.driftClient.getPerpMarketAccount(perpMarketIndex); + const spotMarket = this.driftClient.getSpotMarketAccount( + perpMarket.quoteSpotMarketIndex + ); + if (perpPosition === undefined) { + return ZERO; + } + return getTokenAmount( + perpPosition.isolatedPositionScaledBalance, + spotMarket, + SpotBalanceType.DEPOSIT + ); + } + public getClonedPosition(position: PerpPosition): PerpPosition { const clonedPosition = Object.assign({}, position); return clonedPosition; @@ -506,62 +807,113 @@ export class User { */ public getFreeCollateral( marginCategory: MarginCategory = 'Initial', - enterHighLeverageMode = undefined + enterHighLeverageMode = false, + perpMarketIndex?: number ): BN { - const totalCollateral = this.getTotalCollateral(marginCategory, true); - const marginRequirement = - marginCategory === 'Initial' - ? this.getInitialMarginRequirement(enterHighLeverageMode) - : this.getMaintenanceMarginRequirement(); - const freeCollateral = totalCollateral.sub(marginRequirement); - return freeCollateral.gte(ZERO) ? freeCollateral : ZERO; + const marginCalc = this.getMarginCalculation(marginCategory, { + enteringHighLeverage: enterHighLeverageMode, + }); + + if (perpMarketIndex !== undefined) { + return marginCalc.getIsolatedFreeCollateral(perpMarketIndex); + } else { + return marginCalc.getCrossFreeCollateral(); + } } /** - * @returns The margin requirement of a certain type (Initial or Maintenance) in USDC. : QUOTE_PRECISION + * @deprecated Use the overload that includes { marginType, perpMarketIndex } */ public getMarginRequirement( marginCategory: MarginCategory, liquidationBuffer?: BN, - strict = false, - includeOpenOrders = true, - enteringHighLeverage = undefined + strict?: boolean, + includeOpenOrders?: boolean, + enteringHighLeverage?: boolean + ): BN; + + /** + * Calculates the margin requirement based on the specified parameters. + * + * @param marginCategory - The category of margin to calculate ('Initial' or 'Maintenance'). + * @param liquidationBuffer - Optional buffer amount to consider during liquidation scenarios. + * @param strict - Optional flag to enforce strict margin calculations. + * @param includeOpenOrders - Optional flag to include open orders in the margin calculation. + * @param enteringHighLeverage - Optional flag indicating if the user is entering high leverage mode. + * @param perpMarketIndex - Optional index of the perpetual market. Required if marginType is 'Isolated'. + * + * @returns The calculated margin requirement as a BN (BigNumber). + */ + public getMarginRequirement( + marginCategory: MarginCategory, + liquidationBuffer?: BN, + strict?: boolean, + includeOpenOrders?: boolean, + enteringHighLeverage?: boolean, + perpMarketIndex?: number + ): BN; + + public getMarginRequirement( + marginCategory: MarginCategory, + liquidationBuffer?: BN, + strict?: boolean, + includeOpenOrders?: boolean, + enteringHighLeverage?: boolean, + perpMarketIndex?: number ): BN { - return this.getTotalPerpPositionLiability( - marginCategory, - liquidationBuffer, - includeOpenOrders, + const marginCalc = this.getMarginCalculation(marginCategory, { strict, - enteringHighLeverage - ).add( - this.getSpotMarketLiabilityValue( - undefined, - marginCategory, - liquidationBuffer, - includeOpenOrders, - strict - ) - ); + includeOpenOrders, + enteringHighLeverage, + liquidationBuffer, + }); + + // If perpMarketIndex is provided, compute only for that market index + if (perpMarketIndex !== undefined) { + const isolatedMarginCalculation = + marginCalc.isolatedMarginCalculations.get(perpMarketIndex); + const { marginRequirement } = isolatedMarginCalculation; + + return marginRequirement; + } + + // Default: Cross margin requirement + // TODO: should we be using plus buffer sometimes? + return marginCalc.marginRequirement; } /** * @returns The initial margin requirement in USDC. : QUOTE_PRECISION */ - public getInitialMarginRequirement(enterHighLeverageMode = undefined): BN { + public getInitialMarginRequirement( + enterHighLeverageMode = false, + perpMarketIndex?: number + ): BN { return this.getMarginRequirement( 'Initial', undefined, - true, + false, undefined, - enterHighLeverageMode + enterHighLeverageMode, + perpMarketIndex ); } /** * @returns The maintenance margin requirement in USDC. : QUOTE_PRECISION */ - public getMaintenanceMarginRequirement(liquidationBuffer?: BN): BN { - return this.getMarginRequirement('Maintenance', liquidationBuffer); + public getMaintenanceMarginRequirement( + liquidationBuffer?: BN, + perpMarketIndex?: number + ): BN { + return this.getMarginRequirement( + 'Maintenance', + liquidationBuffer, + true, // strict default + true, // includeOpenOrders default + false, // enteringHighLeverage default + perpMarketIndex + ); } public getActivePerpPositionsForUserAccount( @@ -571,7 +923,8 @@ export class User { (pos) => !pos.baseAssetAmount.eq(ZERO) || !pos.quoteAssetAmount.eq(ZERO) || - !(pos.openOrders == 0) + !(pos.openOrders == 0) || + pos.isolatedPositionScaledBalance.gt(ZERO) ); } @@ -632,6 +985,7 @@ export class User { const market = this.driftClient.getPerpMarketAccount( perpPosition.marketIndex ); + if(!market) return unrealizedPnl; const oraclePriceData = this.getMMOracleDataForPerpMarket( market.marketIndex ); @@ -1144,22 +1498,21 @@ export class User { marginCategory: MarginCategory = 'Initial', strict = false, includeOpenOrders = true, - liquidationBuffer?: BN + liquidationBuffer?: BN, + perpMarketIndex?: number ): BN { - return this.getSpotMarketAssetValue( - undefined, - marginCategory, + const marginCalc = this.getMarginCalculation(marginCategory, { + strict, includeOpenOrders, - strict - ).add( - this.getUnrealizedPNL( - true, - undefined, - marginCategory, - strict, - liquidationBuffer - ) - ); + liquidationBuffer, + }); + + if (perpMarketIndex !== undefined) { + return marginCalc.isolatedMarginCalculations.get(perpMarketIndex) + .totalCollateral; + } + + return marginCalc.totalCollateral; } public getLiquidationBuffer(): BN | undefined { @@ -1177,13 +1530,27 @@ export class User { * calculates User Health by comparing total collateral and maint. margin requirement * @returns : number (value from [0, 100]) */ - public getHealth(): number { - if (this.isBeingLiquidated()) { + public getHealth(perpMarketIndex?: number): number { + const marginCalc = this.getMarginCalculation('Maintenance'); + if (this.isCrossMarginBeingLiquidated(marginCalc) && !perpMarketIndex) { return 0; } - const totalCollateral = this.getTotalCollateral('Maintenance'); - const maintenanceMarginReq = this.getMaintenanceMarginRequirement(); + + let totalCollateral: BN; + let maintenanceMarginReq: BN; + + if (perpMarketIndex) { + const isolatedMarginCalc = + marginCalc.isolatedMarginCalculations.get(perpMarketIndex); + if (isolatedMarginCalc) { + totalCollateral = isolatedMarginCalc.totalCollateral; + maintenanceMarginReq = isolatedMarginCalc.marginRequirement; + } + } else { + totalCollateral = marginCalc.totalCollateral; + maintenanceMarginReq = marginCalc.marginRequirement; + } let health: number; @@ -1219,6 +1586,8 @@ export class User { perpPosition.marketIndex ); + if(!market) return ZERO; + let valuationPrice = this.getOracleDataForPerpMarket( market.marketIndex ).price; @@ -1482,9 +1851,9 @@ export class User { * calculates current user leverage which is (total liability size) / (net asset value) * @returns : Precision TEN_THOUSAND */ - public getLeverage(includeOpenOrders = true): BN { + public getLeverage(includeOpenOrders = true, perpMarketIndex?: number): BN { return this.calculateLeverageFromComponents( - this.getLeverageComponents(includeOpenOrders) + this.getLeverageComponents(includeOpenOrders, undefined, perpMarketIndex) ); } @@ -1512,13 +1881,61 @@ export class User { getLeverageComponents( includeOpenOrders = true, - marginCategory: MarginCategory = undefined + marginCategory: MarginCategory = undefined, + perpMarketIndex?: number ): { perpLiabilityValue: BN; perpPnl: BN; spotAssetValue: BN; spotLiabilityValue: BN; } { + if (perpMarketIndex) { + const perpPosition = this.getPerpPositionOrEmpty(perpMarketIndex); + const perpLiability = this.calculateWeightedPerpPositionLiability( + perpPosition, + marginCategory, + undefined, + includeOpenOrders + ); + const perpMarket = this.driftClient.getPerpMarketAccount( + perpPosition.marketIndex + ); + + const oraclePriceData = this.getOracleDataForPerpMarket( + perpPosition.marketIndex + ); + const quoteSpotMarket = this.driftClient.getSpotMarketAccount( + perpMarket.quoteSpotMarketIndex + ); + const quoteOraclePriceData = this.getOracleDataForSpotMarket( + perpMarket.quoteSpotMarketIndex + ); + const strictOracle = new StrictOraclePrice( + quoteOraclePriceData.price, + quoteOraclePriceData.twap + ); + + const positionUnrealizedPnl = calculatePositionPNL( + perpMarket, + perpPosition, + true, + oraclePriceData + ); + + const spotAssetValue = getStrictTokenValue( + perpPosition.isolatedPositionScaledBalance, + quoteSpotMarket.decimals, + strictOracle + ); + + return { + perpLiabilityValue: perpLiability, + perpPnl: positionUnrealizedPnl, + spotAssetValue, + spotLiabilityValue: ZERO, + }; + } + const perpLiability = this.getTotalPerpPositionLiability( marginCategory, undefined, @@ -1812,35 +2229,87 @@ export class User { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN; + liquidationStatuses: Map<'cross' | number, { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN }>; } { - const liquidationBuffer = this.getLiquidationBuffer(); + // Deprecated signature retained for backward compatibility in type only + // but implementation now delegates to the new Map-based API and returns cross margin status. + const map = this.getLiquidationStatuses(); + const cross = map.get('cross'); + return cross ? { ...cross, liquidationStatuses: map } : { canBeLiquidated: false, marginRequirement: ZERO, totalCollateral: ZERO, liquidationStatuses: map }; + } - const totalCollateral = this.getTotalCollateral( - 'Maintenance', - undefined, - undefined, - liquidationBuffer - ); + /** + * New API: Returns liquidation status for cross and each isolated perp position. + * Map keys: + * - 'cross' for cross margin + * - marketIndex (number) for each isolated perp position + */ + public getLiquidationStatuses(marginCalc?: MarginCalculation): Map<'cross' | number, { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN }> { + // If not provided, use buffer-aware calc for canBeLiquidated checks + if (!marginCalc) { + const liquidationBuffer = this.getLiquidationBuffer(); + marginCalc = this.getMarginCalculation('Maintenance', { liquidationBuffer }); + } - const marginRequirement = - this.getMaintenanceMarginRequirement(liquidationBuffer); - const canBeLiquidated = totalCollateral.lt(marginRequirement); + const result = new Map<'cross' | number, { + canBeLiquidated: boolean; + marginRequirement: BN; + totalCollateral: BN; + }>(); + + // Cross margin status + const crossTotalCollateral = marginCalc.totalCollateral; + const crossMarginRequirement = marginCalc.marginRequirement; + result.set('cross', { + canBeLiquidated: crossTotalCollateral.lt(crossMarginRequirement), + marginRequirement: crossMarginRequirement, + totalCollateral: crossTotalCollateral, + }); - return { - canBeLiquidated, - marginRequirement, - totalCollateral, - }; + // Isolated positions status + for (const [marketIndex, isoCalc] of marginCalc.isolatedMarginCalculations) { + const isoTotalCollateral = isoCalc.totalCollateral; + const isoMarginRequirement = isoCalc.marginRequirement; + result.set(marketIndex, { + canBeLiquidated: isoTotalCollateral.lt(isoMarginRequirement), + marginRequirement: isoMarginRequirement, + totalCollateral: isoTotalCollateral, + }); + } + + return result; } - public isBeingLiquidated(): boolean { - return ( + public isBeingLiquidated(marginCalc?: MarginCalculation): boolean { + // Consider on-chain flags OR computed margin status (cross or any isolated) + const hasOnChainFlag = (this.getUserAccount().status & - (UserStatus.BEING_LIQUIDATED | UserStatus.BANKRUPT)) > - 0 + (UserStatus.BEING_LIQUIDATED | UserStatus.BANKRUPT)) > 0; + const calc = marginCalc ?? this.getMarginCalculation('Maintenance'); + return ( + hasOnChainFlag || + this.isCrossMarginBeingLiquidated(calc) || + this.isIsolatedMarginBeingLiquidated(calc) ); } + /** Returns true if cross margin is currently below maintenance requirement (no buffer). */ + public isCrossMarginBeingLiquidated(marginCalc?: MarginCalculation): boolean { + const calc = marginCalc ?? this.getMarginCalculation('Maintenance'); + return calc.totalCollateral.lt(calc.marginRequirement); + } + + /** Returns true if any isolated perp position is currently below its maintenance requirement (no buffer). */ + public isIsolatedMarginBeingLiquidated(marginCalc?: MarginCalculation): boolean { + const calc = marginCalc ?? this.getMarginCalculation('Maintenance'); + for (const [, isoCalc] of calc.isolatedMarginCalculations) { + if (isoCalc.totalCollateral.lt(isoCalc.marginRequirement)) { + return true; + } + } + return false; + } + public hasStatus(status: UserStatus): boolean { return (this.getUserAccount().status & status) > 0; } @@ -1997,8 +2466,61 @@ export class User { marginCategory: MarginCategory = 'Maintenance', includeOpenOrders = false, offsetCollateral = ZERO, - enteringHighLeverage = undefined + enteringHighLeverage = false, + marginType?: MarginType ): BN { + const market = this.driftClient.getPerpMarketAccount(marketIndex); + + const oracle = + this.driftClient.getPerpMarketAccount(marketIndex).amm.oracle; + + const oraclePrice = + this.driftClient.getOracleDataForPerpMarket(marketIndex).price; + + const currentPerpPosition = this.getPerpPositionOrEmpty(marketIndex); + + if (marginType === 'Isolated') { + const marginCalculation = this.getMarginCalculation(marginCategory, { + strict: false, + includeOpenOrders, + enteringHighLeverage, + }); + const isolatedMarginCalculation = + marginCalculation.isolatedMarginCalculations.get(marketIndex); + const { totalCollateral, marginRequirement } = isolatedMarginCalculation; + + const freeCollateral = BN.max( + ZERO, + totalCollateral.sub(marginRequirement) + ).add(offsetCollateral); + + const freeCollateralDelta = this.calculateFreeCollateralDeltaForPerp( + market, + currentPerpPosition, + positionBaseSizeChange, + oraclePrice, + marginCategory, + includeOpenOrders, + enteringHighLeverage + ); + + if (freeCollateralDelta.eq(ZERO)) { + return new BN(-1); + } + + const liqPriceDelta = freeCollateral + .mul(QUOTE_PRECISION) + .div(freeCollateralDelta); + + const liqPrice = oraclePrice.sub(liqPriceDelta); + + if (liqPrice.lt(ZERO)) { + return new BN(-1); + } + + return liqPrice; + } + const totalCollateral = this.getTotalCollateral( marginCategory, false, @@ -2016,15 +2538,6 @@ export class User { totalCollateral.sub(marginRequirement) ).add(offsetCollateral); - const oracle = - this.driftClient.getPerpMarketAccount(marketIndex).amm.oracle; - - const oraclePrice = - this.driftClient.getOracleDataForPerpMarket(marketIndex).price; - - const market = this.driftClient.getPerpMarketAccount(marketIndex); - const currentPerpPosition = this.getPerpPositionOrEmpty(marketIndex); - positionBaseSizeChange = standardizeBaseAssetAmount( positionBaseSizeChange, market.amm.orderStepSize @@ -3893,4 +4406,7 @@ export class User { activeSpotPositions: activeSpotMarkets, }; } + private isPerpPositionIsolated(perpPosition: PerpPosition): boolean { + return (perpPosition.positionFlag & PositionFlag.IsolatedPosition) !== 0; + } } diff --git a/sdk/tests/amm/test.ts b/sdk/tests/amm/test.ts index ab849c57d6..b4377f4066 100644 --- a/sdk/tests/amm/test.ts +++ b/sdk/tests/amm/test.ts @@ -279,7 +279,7 @@ describe('AMM Tests', () => { longIntensity, shortIntensity, volume24H, - 0 + 0, ); const l1 = spreads[0]; const s1 = spreads[1]; diff --git a/sdk/tests/dlob/helpers.ts b/sdk/tests/dlob/helpers.ts index d682f5e757..6873c77dc1 100644 --- a/sdk/tests/dlob/helpers.ts +++ b/sdk/tests/dlob/helpers.ts @@ -27,6 +27,8 @@ import { DataAndSlot, } from '../../src'; import { EventEmitter } from 'events'; +import StrictEventEmitter from 'strict-event-emitter-types'; +import { UserEvents } from '../../src/accounts/types'; export const mockPerpPosition: PerpPosition = { baseAssetAmount: new BN(0), @@ -44,6 +46,8 @@ export const mockPerpPosition: PerpPosition = { lastBaseAssetAmountPerLp: new BN(0), lastQuoteAssetAmountPerLp: new BN(0), perLpBase: 0, + positionFlag: 0, + isolatedPositionScaledBalance: new BN(0), maxMarginRatio: 1, }; @@ -670,6 +674,7 @@ export class MockUserMap implements UserMapInterface { wallet: new Wallet(new Keypair()), programID: PublicKey.default, }); + this.eventEmitter = new EventEmitter(); } public async subscribe(): Promise {} diff --git a/sdk/tests/user/getMarginCalculation.ts b/sdk/tests/user/getMarginCalculation.ts new file mode 100644 index 0000000000..afc2111996 --- /dev/null +++ b/sdk/tests/user/getMarginCalculation.ts @@ -0,0 +1,393 @@ +import { + BN, + ZERO, + User, + UserAccount, + PublicKey, + PerpMarketAccount, + SpotMarketAccount, + PRICE_PRECISION, + OraclePriceData, + BASE_PRECISION, + QUOTE_PRECISION, + SPOT_MARKET_BALANCE_PRECISION, + SpotBalanceType, + MARGIN_PRECISION, + OPEN_ORDER_MARGIN_REQUIREMENT, + SPOT_MARKET_WEIGHT_PRECISION, + PositionFlag, +} from '../../src'; +import { MockUserMap, mockPerpMarkets, mockSpotMarkets } from '../dlob/helpers'; +import { assert } from '../../src/assert/assert'; +import { mockUserAccount as baseMockUserAccount } from './helpers'; +import * as _ from 'lodash'; + +async function makeMockUser( + myMockPerpMarkets: Array, + myMockSpotMarkets: Array, + myMockUserAccount: UserAccount, + perpOraclePriceList: number[], + spotOraclePriceList: number[] +): Promise { + const umap = new MockUserMap(); + const mockUser: User = await umap.mustGet('1'); + mockUser._isSubscribed = true; + mockUser.driftClient._isSubscribed = true; + mockUser.driftClient.accountSubscriber.isSubscribed = true; + + const oraclePriceMap: Record = {}; + for (let i = 0; i < myMockPerpMarkets.length; i++) { + oraclePriceMap[myMockPerpMarkets[i].amm.oracle.toString()] = + perpOraclePriceList[i] ?? 1; + } + for (let i = 0; i < myMockSpotMarkets.length; i++) { + oraclePriceMap[myMockSpotMarkets[i].oracle.toString()] = + spotOraclePriceList[i] ?? 1; + } + + function getMockUserAccount(): UserAccount { + return myMockUserAccount; + } + function getMockPerpMarket(marketIndex: number): PerpMarketAccount { + return myMockPerpMarkets[marketIndex]; + } + function getMockSpotMarket(marketIndex: number): SpotMarketAccount { + return myMockSpotMarkets[marketIndex]; + } + function getMockOracle(oracleKey: PublicKey) { + const data: OraclePriceData = { + price: new BN( + (oraclePriceMap[oracleKey.toString()] ?? 1) * + PRICE_PRECISION.toNumber() + ), + slot: new BN(0), + confidence: new BN(1), + hasSufficientNumberOfDataPoints: true, + }; + return { data, slot: 0 }; + } + function getOracleDataForPerpMarket(marketIndex: number) { + const oracle = getMockPerpMarket(marketIndex).amm.oracle; + return getMockOracle(oracle).data; + } + function getOracleDataForSpotMarket(marketIndex: number) { + const oracle = getMockSpotMarket(marketIndex).oracle; + return getMockOracle(oracle).data; + } + + mockUser.getUserAccount = getMockUserAccount; + mockUser.driftClient.getPerpMarketAccount = getMockPerpMarket as any; + mockUser.driftClient.getSpotMarketAccount = getMockSpotMarket as any; + mockUser.driftClient.getOraclePriceDataAndSlot = getMockOracle as any; + mockUser.driftClient.getOracleDataForPerpMarket = getOracleDataForPerpMarket as any; + mockUser.driftClient.getOracleDataForSpotMarket = getOracleDataForSpotMarket as any; + return mockUser; +} + +describe('getMarginCalculation snapshot', () => { + it('empty account returns zeroed snapshot', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Initial'); + assert(calc.totalCollateral.eq(ZERO)); + assert(calc.marginRequirement.eq(ZERO)); + assert(calc.numSpotLiabilities === 0); + assert(calc.numPerpLiabilities === 0); + }); + + it('quote deposit increases totalCollateral, no requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; + myMockUserAccount.spotPositions[0].scaledBalance = new BN( + 10000 * SPOT_MARKET_BALANCE_PRECISION.toNumber() + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Initial'); + const expected = new BN('10000000000'); // $10k + assert(calc.totalCollateral.eq(expected)); + assert(calc.marginRequirement.eq(ZERO)); + }); + + it('quote borrow increases requirement and buffer applies', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // Borrow 100 quote + myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.BORROW; + myMockUserAccount.spotPositions[0].scaledBalance = new BN( + 100 * SPOT_MARKET_BALANCE_PRECISION.toNumber() + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const tenPercent = new BN(1000); + const calc = user.getMarginCalculation('Initial', { + liquidationBuffer: tenPercent, + }); + const liability = new BN(100).mul(QUOTE_PRECISION); // $100 + assert(calc.totalCollateral.eq(ZERO)); + assert(calc.marginRequirement.eq(liability)); + assert( + calc.marginRequirementPlusBuffer.eq( + liability.div(new BN(10)).add(calc.marginRequirement) // 10% of liability + margin requirement + ) + ); + assert(calc.numSpotLiabilities === 1); + }); + + it('non-quote spot open orders add IM', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // Market 1 (e.g., SOL) with 2 open orders + myMockUserAccount.spotPositions[1].marketIndex = 1; + myMockUserAccount.spotPositions[1].openOrders = 2; + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Initial'); + const expectedIM = new BN(2).mul(OPEN_ORDER_MARGIN_REQUIREMENT); + assert(calc.marginRequirement.eq(expectedIM)); + }); + + it('perp long liability reflects maintenance requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // 20 base long, -$10 quote (liability) + myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(20).mul( + BASE_PRECISION + ); + myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN(-10).mul( + QUOTE_PRECISION + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Maintenance'); + // From existing liquidation test expectations: 2_000_000 + assert(calc.marginRequirement.eq(new BN('2000000'))); + }); + + it.only('maker position reducing: collateral equals maintenance requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(200000000).mul( + BASE_PRECISION + ); + myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN(-180000000).mul( + QUOTE_PRECISION + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Maintenance'); + console.log('calc.marginRequirement', calc.marginRequirement.toString()); + console.log('calc.totalCollateral', calc.totalCollateral.toString()); + assert(calc.marginRequirement.eq(calc.totalCollateral)); + }); + + it('maker reducing after simulated fill: collateral equals maintenance requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + + // Build maker and taker accounts + const makerAccount = _.cloneDeep(baseMockUserAccount); + const takerAccount = _.cloneDeep(baseMockUserAccount); + + // Oracle price = 1 for perp and spot + const perpOracles = [1, 1, 1, 1, 1, 1, 1, 1]; + const spotOracles = [1, 1, 1, 1, 1, 1, 1, 1]; + + // Pre-fill: maker has 21 base long at entry 1 ($21 notional), taker flat + makerAccount.perpPositions[0].baseAssetAmount = new BN(21).mul(BASE_PRECISION); + makerAccount.perpPositions[0].quoteEntryAmount = new BN(-21).mul(QUOTE_PRECISION); + makerAccount.perpPositions[0].quoteBreakEvenAmount = new BN(-21).mul(QUOTE_PRECISION); + // Provide exactly $2 in quote collateral to equal 10% maintenance of 20 notional post-fill + makerAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; + makerAccount.spotPositions[0].scaledBalance = new BN(2).mul( + SPOT_MARKET_BALANCE_PRECISION + ); + + // Simulate fill: maker sells 1 base to taker at price = oracle = 1 + // Post-fill maker position: 20 base long with zero unrealized PnL + const maker: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + makerAccount, + perpOracles, + spotOracles + ); + const taker: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + takerAccount, + perpOracles, + spotOracles + ); + + // Apply synthetic trade deltas to both user accounts + // Maker: base 21 -> 20; taker: base 0 -> 1. Use quote deltas consistent with price 1, fee 0 + maker.getUserAccount().perpPositions[0].baseAssetAmount = new BN(20).mul( + BASE_PRECISION + ); + maker.getUserAccount().perpPositions[0].quoteEntryAmount = new BN(-20).mul( + QUOTE_PRECISION + ); + maker.getUserAccount().perpPositions[0].quoteBreakEvenAmount = new BN(-20).mul( + QUOTE_PRECISION + ); + // Align quoteAssetAmount with base value so unrealized PnL = 0 at price 1 + maker.getUserAccount().perpPositions[0].quoteAssetAmount = new BN(-20).mul( + QUOTE_PRECISION + ); + + taker.getUserAccount().perpPositions[0].baseAssetAmount = new BN(1).mul( + BASE_PRECISION + ); + taker.getUserAccount().perpPositions[0].quoteEntryAmount = new BN(-1).mul( + QUOTE_PRECISION + ); + taker.getUserAccount().perpPositions[0].quoteBreakEvenAmount = new BN(-1).mul( + QUOTE_PRECISION + ); + // Also set taker's quoteAssetAmount consistently + taker.getUserAccount().perpPositions[0].quoteAssetAmount = new BN(-1).mul( + QUOTE_PRECISION + ); + + const makerCalc = maker.getMarginCalculation('Maintenance'); + assert(makerCalc.marginRequirement.eq(makerCalc.totalCollateral)); + assert(makerCalc.marginRequirement.gt(ZERO)); + }); + + it('isolated position margin requirement (SDK parity)', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + myMockSpotMarkets[0].oracle = new PublicKey(2); + myMockSpotMarkets[1].oracle = new PublicKey(5); + myMockPerpMarkets[0].amm.oracle = new PublicKey(5); + + // Configure perp market 0 ratios to match on-chain test + myMockPerpMarkets[0].marginRatioInitial = 1000; // 10% + myMockPerpMarkets[0].marginRatioMaintenance = 500; // 5% + + // Configure spot market 1 (e.g., SOL) weights to match on-chain test + myMockSpotMarkets[1].initialAssetWeight = + (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 8) / 10; // 0.8 + myMockSpotMarkets[1].maintenanceAssetWeight = + (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 9) / 10; // 0.9 + myMockSpotMarkets[1].initialLiabilityWeight = + (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 12) / 10; // 1.2 + myMockSpotMarkets[1].maintenanceLiabilityWeight = + (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 11) / 10; // 1.1 + + // ---------- Cross margin only (spot positions) ---------- + const crossAccount = _.cloneDeep(baseMockUserAccount); + // USDC deposit: $20,000 + crossAccount.spotPositions[0].marketIndex = 0; + crossAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; + crossAccount.spotPositions[0].scaledBalance = new BN(20000).mul( + SPOT_MARKET_BALANCE_PRECISION + ); + // SOL borrow: 100 units + crossAccount.spotPositions[1].marketIndex = 1; + crossAccount.spotPositions[1].balanceType = SpotBalanceType.BORROW; + crossAccount.spotPositions[1].scaledBalance = new BN(100).mul( + SPOT_MARKET_BALANCE_PRECISION + ); + // No perp exposure in cross calc + crossAccount.perpPositions[0].baseAssetAmount = new BN(100 * BASE_PRECISION.toNumber()); + crossAccount.perpPositions[0].quoteAssetAmount = new BN(-11000 * QUOTE_PRECISION.toNumber()); + crossAccount.perpPositions[0].positionFlag = PositionFlag.IsolatedPosition; + crossAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(100).mul( + SPOT_MARKET_BALANCE_PRECISION + ); + + const userCross: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + crossAccount, + [100, 1, 1, 1, 1, 1, 1, 1], // perp oracle for market 0 = 100 + [1, 100, 1, 1, 1, 1, 1, 1] // spot oracle: usdc=1, sol=100 + ); + + const crossCalc = userCross.getMarginCalculation('Initial'); + const isolatedMarginCalc = crossCalc.isolatedMarginCalculations.get(0); + // Expect: cross MR from SOL borrow: 100 * $100 = $10,000 * 1.2 = $12,000 + assert(crossCalc.marginRequirement.eq(new BN('12000000000'))); + // Expect: cross total collateral from USDC deposit only = $20,000 + assert(crossCalc.totalCollateral.eq(new BN('20000000000'))); + // Meets cross margin requirement + assert(crossCalc.marginRequirement.lte(crossCalc.totalCollateral)); + + assert(isolatedMarginCalc?.marginRequirement.eq(new BN('1000000000'))); + assert(isolatedMarginCalc?.totalCollateral.eq(new BN('-900000000'))); + // With 10% buffer + const tenPct = new BN(1000); + const crossCalcBuf = userCross.getMarginCalculation('Initial', { + liquidationBuffer: tenPct, + }); + assert(crossCalcBuf.marginRequirementPlusBuffer.eq(new BN('13000000000'))); // replicate 10% buffer + const crossTotalPlusBuffer = crossCalcBuf.totalCollateral.add( + crossCalcBuf.totalCollateralBuffer + ); + assert(crossTotalPlusBuffer.eq(new BN('20000000000'))); + + const isoPosition = crossCalcBuf.isolatedMarginCalculations.get(0); + assert(isoPosition?.marginRequirementPlusBuffer.eq(new BN('2000000000'))); + assert(isoPosition?.totalCollateralBuffer.add(isoPosition?.totalCollateral).eq(new BN('-1000000000'))); + }); +}); + + diff --git a/sdk/tests/user/test.ts b/sdk/tests/user/test.ts index 6c431b7226..990abc473e 100644 --- a/sdk/tests/user/test.ts +++ b/sdk/tests/user/test.ts @@ -49,7 +49,6 @@ async function makeMockUser( oraclePriceMap[myMockSpotMarkets[i].oracle.toString()] = spotOraclePriceList[i]; } - // console.log(oraclePriceMap); function getMockUserAccount(): UserAccount { return myMockUserAccount; @@ -61,12 +60,6 @@ async function makeMockUser( return myMockSpotMarkets[marketIndex]; } function getMockOracle(oracleKey: PublicKey) { - // console.log('oracleKey.toString():', oracleKey.toString()); - // console.log( - // 'oraclePriceMap[oracleKey.toString()]:', - // oraclePriceMap[oracleKey.toString()] - // ); - const QUOTE_ORACLE_PRICE_DATA: OraclePriceData = { price: new BN( oraclePriceMap[oracleKey.toString()] * PRICE_PRECISION.toNumber() diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index 78b954d28f..d695326608 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -37,6 +37,7 @@ test_files=( highLeverageMode.ts ifRebalance.ts insuranceFundStake.ts + isolatedPositionDriftClient.ts liquidateBorrowForPerpPnl.ts liquidatePerp.ts liquidatePerpWithFill.ts diff --git a/tests/isolatedPositionDriftClient.ts b/tests/isolatedPositionDriftClient.ts new file mode 100644 index 0000000000..644ae91308 --- /dev/null +++ b/tests/isolatedPositionDriftClient.ts @@ -0,0 +1,547 @@ +import * as anchor from '@coral-xyz/anchor'; +import { assert } from 'chai'; +import { BN, OracleSource, ZERO } from '../sdk'; + +import { Program } from '@coral-xyz/anchor'; + +import { PublicKey } from '@solana/web3.js'; + +import { TestClient, PositionDirection, EventSubscriber } from '../sdk/src'; + +import { + mockUSDCMint, + mockUserUSDCAccount, + mockOracleNoProgram, + setFeedPriceNoProgram, + initializeQuoteSpotMarket, +} from './testHelpers'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; + +describe('drift client', () => { + const chProgram = anchor.workspace.Drift as Program; + + let driftClient: TestClient; + let eventSubscriber: EventSubscriber; + + let bankrunContextWrapper: BankrunContextWrapper; + + let bulkAccountLoader: TestBulkAccountLoader; + + let userAccountPublicKey: PublicKey; + + let usdcMint; + let userUSDCAccount; + + let solUsd; + + // ammInvariant == k == x * y + const mantissaSqrtScale = new BN(100000); + const ammInitialQuoteAssetAmount = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetAmount = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + + const usdcAmount = new BN(10 * 10 ** 6); + + before(async () => { + const context = await startAnchor('', [], []); + + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + userUSDCAccount = await mockUserUSDCAccount( + usdcMint, + usdcAmount, + bankrunContextWrapper + ); + + solUsd = await mockOracleNoProgram(bankrunContextWrapper, 1); + + eventSubscriber = new EventSubscriber( + bankrunContextWrapper.connection.toConnection(), + chProgram + ); + + await eventSubscriber.subscribe(); + + driftClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: bankrunContextWrapper.provider.wallet, + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + perpMarketIndexes: [0], + spotMarketIndexes: [0], + subAccountIds: [], + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + userStats: true, + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + + await driftClient.initialize(usdcMint.publicKey, true); + + await driftClient.subscribe(); + await driftClient.updatePerpAuctionDuration(new BN(0)); + + await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); + + const periodicity = new BN(60 * 60); // 1 HOUR + + await driftClient.initializePerpMarket( + 0, + solUsd, + ammInitialBaseAssetAmount, + ammInitialQuoteAssetAmount, + periodicity + ); + + await driftClient.updatePerpMarketStepSizeAndTickSize( + 0, + new BN(1), + new BN(1) + ); + }); + + after(async () => { + await driftClient.unsubscribe(); + await eventSubscriber.unsubscribe(); + }); + + it('Initialize user account and deposit collateral', async () => { + await driftClient.initializeUserAccount(); + + userAccountPublicKey = await driftClient.getUserAccountPublicKey(); + + const txSig = await driftClient.depositIntoIsolatedPerpPosition( + usdcAmount, + 0, + userUSDCAccount.publicKey + ); + + const depositTokenAmount = + driftClient.getIsolatedPerpPositionTokenAmount(0); + console.log('depositTokenAmount', depositTokenAmount.toString()); + assert(depositTokenAmount.eq(usdcAmount)); + + // Check that drift collateral account has proper collateral + const quoteSpotVault = + await bankrunContextWrapper.connection.getTokenAccount( + driftClient.getQuoteSpotMarketAccount().vault + ); + + assert.ok(new BN(Number(quoteSpotVault.amount)).eq(usdcAmount)); + + await eventSubscriber.awaitTx(txSig); + const depositRecord = eventSubscriber.getEventsArray('DepositRecord')[0]; + + assert.ok( + depositRecord.userAuthority.equals( + bankrunContextWrapper.provider.wallet.publicKey + ) + ); + assert.ok(depositRecord.user.equals(userAccountPublicKey)); + + assert.ok( + JSON.stringify(depositRecord.direction) === + JSON.stringify({ deposit: {} }) + ); + assert.ok(depositRecord.amount.eq(new BN(10000000))); + }); + + it('Transfer isolated perp position deposit', async () => { + await driftClient.transferIsolatedPerpPositionDeposit(usdcAmount.neg(), 0); + + const quoteAssetTokenAmount = + driftClient.getIsolatedPerpPositionTokenAmount(0); + assert(quoteAssetTokenAmount.eq(ZERO)); + + const quoteTokenAmount = driftClient.getQuoteAssetTokenAmount(); + assert(quoteTokenAmount.eq(usdcAmount)); + + await driftClient.transferIsolatedPerpPositionDeposit(usdcAmount, 0); + + const quoteAssetTokenAmount2 = + driftClient.getIsolatedPerpPositionTokenAmount(0); + assert(quoteAssetTokenAmount2.eq(usdcAmount)); + + const quoteTokenAmoun2 = driftClient.getQuoteAssetTokenAmount(); + assert(quoteTokenAmoun2.eq(ZERO)); + }); + + it('Withdraw Collateral', async () => { + await driftClient.withdrawFromIsolatedPerpPosition( + usdcAmount, + 0, + userUSDCAccount.publicKey + ); + + await driftClient.fetchAccounts(); + assert(driftClient.getIsolatedPerpPositionTokenAmount(0).eq(ZERO)); + + // Check that drift collateral account has proper collateral] + const quoteSpotVault = + await bankrunContextWrapper.connection.getTokenAccount( + driftClient.getQuoteSpotMarketAccount().vault + ); + + assert.ok(new BN(Number(quoteSpotVault.amount)).eq(ZERO)); + + const userUSDCtoken = + await bankrunContextWrapper.connection.getTokenAccount( + userUSDCAccount.publicKey + ); + assert.ok(new BN(Number(userUSDCtoken.amount)).eq(usdcAmount)); + + const depositRecord = eventSubscriber.getEventsArray('DepositRecord')[0]; + + assert.ok( + depositRecord.userAuthority.equals( + bankrunContextWrapper.provider.wallet.publicKey + ) + ); + assert.ok(depositRecord.user.equals(userAccountPublicKey)); + + assert.ok( + JSON.stringify(depositRecord.direction) === + JSON.stringify({ withdraw: {} }) + ); + assert.ok(depositRecord.amount.eq(new BN(10000000))); + }); + + it('Long from 0 position', async () => { + // Re-Deposit USDC, assuming we have 0 balance here + await driftClient.depositIntoIsolatedPerpPosition( + usdcAmount, + 0, + userUSDCAccount.publicKey + ); + + const marketIndex = 0; + const baseAssetAmount = new BN(48000000000); + const txSig = await driftClient.openPosition( + PositionDirection.LONG, + baseAssetAmount, + marketIndex + ); + bankrunContextWrapper.connection.printTxLogs(txSig); + + const marketData = driftClient.getPerpMarketAccount(0); + await setFeedPriceNoProgram( + bankrunContextWrapper, + 1.01, + marketData.amm.oracle + ); + + const orderR = eventSubscriber.getEventsArray('OrderActionRecord')[0]; + console.log(orderR.takerFee.toString()); + console.log(orderR.baseAssetAmountFilled.toString()); + + const user: any = await driftClient.program.account.user.fetch( + userAccountPublicKey + ); + + console.log( + 'getQuoteAssetTokenAmount:', + driftClient.getIsolatedPerpPositionTokenAmount(0).toString() + ); + assert( + driftClient.getIsolatedPerpPositionTokenAmount(0).eq(new BN(10000000)) + ); + assert( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.eq(new BN(48001)) + ); + + assert.ok(user.perpPositions[0].quoteEntryAmount.eq(new BN(-48000001))); + assert.ok(user.perpPositions[0].quoteBreakEvenAmount.eq(new BN(-48048002))); + assert.ok(user.perpPositions[0].baseAssetAmount.eq(new BN(48000000000))); + assert.ok(user.perpPositions[0].positionFlag === 1); + + const market = driftClient.getPerpMarketAccount(0); + console.log(market.amm.baseAssetAmountWithAmm.toNumber()); + console.log(market); + + assert.ok(market.amm.baseAssetAmountWithAmm.eq(new BN(48000000000))); + console.log(market.amm.totalFee.toString()); + assert.ok(market.amm.totalFee.eq(new BN(48001))); + assert.ok(market.amm.totalFeeMinusDistributions.eq(new BN(48001))); + + const orderActionRecord = + eventSubscriber.getEventsArray('OrderActionRecord')[0]; + + assert.ok(orderActionRecord.taker.equals(userAccountPublicKey)); + assert.ok(orderActionRecord.fillRecordId.eq(new BN(1))); + assert.ok(orderActionRecord.baseAssetAmountFilled.eq(new BN(48000000000))); + assert.ok(orderActionRecord.quoteAssetAmountFilled.eq(new BN(48000001))); + assert.ok(orderActionRecord.marketIndex === marketIndex); + + assert.ok(orderActionRecord.takerExistingQuoteEntryAmount === null); + assert.ok(orderActionRecord.takerExistingBaseAssetAmount === null); + + assert(driftClient.getPerpMarketAccount(0).nextFillRecordId.eq(new BN(2))); + }); + + it('Withdraw fails due to insufficient collateral', async () => { + // lil hack to stop printing errors + const oldConsoleLog = console.log; + const oldConsoleError = console.error; + console.log = function () { + const _noop = ''; + }; + console.error = function () { + const _noop = ''; + }; + try { + await driftClient.withdrawFromIsolatedPerpPosition( + usdcAmount, + 0, + userUSDCAccount.publicKey + ); + assert(false, 'Withdrawal succeeded'); + } catch (e) { + assert(true); + } finally { + console.log = oldConsoleLog; + console.error = oldConsoleError; + } + }); + + it('Reduce long position', async () => { + const marketIndex = 0; + const baseAssetAmount = new BN(24000000000); + await driftClient.openPosition( + PositionDirection.SHORT, + baseAssetAmount, + marketIndex + ); + + await driftClient.fetchAccounts(); + + await driftClient.fetchAccounts(); + const user = driftClient.getUserAccount(); + console.log( + 'quoteAssetAmount:', + user.perpPositions[0].quoteAssetAmount.toNumber() + ); + console.log( + 'quoteBreakEvenAmount:', + user.perpPositions[0].quoteBreakEvenAmount.toNumber() + ); + + assert.ok(user.perpPositions[0].quoteAssetAmount.eq(new BN(-24072002))); + assert.ok(user.perpPositions[0].quoteEntryAmount.eq(new BN(-24000001))); + assert.ok(user.perpPositions[0].quoteBreakEvenAmount.eq(new BN(-24048001))); + + assert.ok(user.perpPositions[0].baseAssetAmount.eq(new BN(24000000000))); + console.log(driftClient.getIsolatedPerpPositionTokenAmount(0).toString()); + assert.ok( + driftClient.getIsolatedPerpPositionTokenAmount(0).eq(new BN(10000000)) + ); + console.log( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.toString() + ); + assert( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.eq(new BN(72001)) + ); + + const market = driftClient.getPerpMarketAccount(0); + assert.ok(market.amm.baseAssetAmountWithAmm.eq(new BN(24000000000))); + assert.ok(market.amm.totalFee.eq(new BN(72001))); + assert.ok(market.amm.totalFeeMinusDistributions.eq(new BN(72001))); + + const orderActionRecord = + eventSubscriber.getEventsArray('OrderActionRecord')[0]; + assert.ok(orderActionRecord.taker.equals(userAccountPublicKey)); + assert.ok(orderActionRecord.fillRecordId.eq(new BN(2))); + assert.ok(orderActionRecord.baseAssetAmountFilled.eq(new BN(24000000000))); + assert.ok(orderActionRecord.quoteAssetAmountFilled.eq(new BN(24000000))); + assert.ok(orderActionRecord.marketIndex === 0); + assert.ok( + orderActionRecord.takerExistingQuoteEntryAmount.eq(new BN(24000000)) + ); + assert.ok(orderActionRecord.takerExistingBaseAssetAmount === null); + }); + + it('Reverse long position', async () => { + const marketData = driftClient.getPerpMarketAccount(0); + await setFeedPriceNoProgram( + bankrunContextWrapper, + 1.0, + marketData.amm.oracle + ); + + const baseAssetAmount = new BN(48000000000); + await driftClient.openPosition(PositionDirection.SHORT, baseAssetAmount, 0); + + await driftClient.fetchAccounts(); + await driftClient.settlePNL( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0 + ); + + await driftClient.fetchAccounts(); + const user = driftClient.getUserAccount(); + console.log( + 'quoteAssetAmount:', + user.perpPositions[0].quoteAssetAmount.toNumber() + ); + console.log( + 'quoteBreakEvenAmount:', + user.perpPositions[0].quoteBreakEvenAmount.toNumber() + ); + console.log(driftClient.getIsolatedPerpPositionTokenAmount(0).toString()); + console.log( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.toString() + ); + assert.ok( + driftClient.getIsolatedPerpPositionTokenAmount(0).eq(new BN(9879998)) + ); + assert( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.eq(new BN(120001)) + ); + console.log(user.perpPositions[0].quoteBreakEvenAmount.toString()); + console.log(user.perpPositions[0].quoteAssetAmount.toString()); + + assert.ok(user.perpPositions[0].quoteEntryAmount.eq(new BN(24000000))); + assert.ok(user.perpPositions[0].quoteBreakEvenAmount.eq(new BN(23952000))); + assert.ok(user.perpPositions[0].quoteAssetAmount.eq(new BN(24000000))); + console.log(user.perpPositions[0].baseAssetAmount.toString()); + assert.ok(user.perpPositions[0].baseAssetAmount.eq(new BN(-24000000000))); + + const market = driftClient.getPerpMarketAccount(0); + assert.ok(market.amm.baseAssetAmountWithAmm.eq(new BN(-24000000000))); + assert.ok(market.amm.totalFee.eq(new BN(120001))); + assert.ok(market.amm.totalFeeMinusDistributions.eq(new BN(120001))); + + const orderActionRecord = + eventSubscriber.getEventsArray('OrderActionRecord')[0]; + assert.ok(orderActionRecord.taker.equals(userAccountPublicKey)); + assert.ok(orderActionRecord.fillRecordId.eq(new BN(3))); + console.log(orderActionRecord.baseAssetAmountFilled.toNumber()); + assert.ok(orderActionRecord.baseAssetAmountFilled.eq(new BN(48000000000))); + assert.ok(orderActionRecord.quoteAssetAmountFilled.eq(new BN(48000000))); + + assert.ok( + orderActionRecord.takerExistingQuoteEntryAmount.eq(new BN(24000001)) + ); + assert.ok( + orderActionRecord.takerExistingBaseAssetAmount.eq(new BN(24000000000)) + ); + + assert.ok(orderActionRecord.marketIndex === 0); + }); + + it('Close position', async () => { + const marketIndex = 0; + await driftClient.closePosition(marketIndex); + + await driftClient.settlePNL( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + marketIndex + ); + + const user: any = await driftClient.program.account.user.fetch( + userAccountPublicKey + ); + assert.ok(user.perpPositions[0].quoteBreakEvenAmount.eq(new BN(0))); + assert.ok(user.perpPositions[0].baseAssetAmount.eq(new BN(0))); + console.log(driftClient.getIsolatedPerpPositionTokenAmount(0).toString()); + assert.ok( + driftClient.getIsolatedPerpPositionTokenAmount(0).eq(new BN(9855998)) + ); + console.log( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.toString() + ); + assert( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.eq(new BN(144001)) + ); + + const market = driftClient.getPerpMarketAccount(0); + assert.ok(market.amm.baseAssetAmountWithAmm.eq(new BN(0))); + assert.ok(market.amm.totalFee.eq(new BN(144001))); + assert.ok(market.amm.totalFeeMinusDistributions.eq(new BN(144001))); + + const orderActionRecord = + eventSubscriber.getEventsArray('OrderActionRecord')[0]; + + assert.ok(orderActionRecord.taker.equals(userAccountPublicKey)); + assert.ok(orderActionRecord.fillRecordId.eq(new BN(4))); + assert.ok(orderActionRecord.baseAssetAmountFilled.eq(new BN(24000000000))); + assert.ok(orderActionRecord.quoteAssetAmountFilled.eq(new BN(24000000))); + assert.ok(orderActionRecord.marketIndex === 0); + + assert.ok( + orderActionRecord.takerExistingQuoteEntryAmount.eq(new BN(24000000)) + ); + assert.ok(orderActionRecord.takerExistingBaseAssetAmount === null); + }); + + it('Open short position', async () => { + const baseAssetAmount = new BN(48000000000); + await driftClient.openPosition(PositionDirection.SHORT, baseAssetAmount, 0); + + await driftClient.settlePNL( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0 + ); + + const user = await driftClient.program.account.user.fetch( + userAccountPublicKey + ); + assert.ok(user.perpPositions[0].positionFlag === 1); + console.log(user.perpPositions[0].quoteBreakEvenAmount.toString()); + assert.ok(user.perpPositions[0].quoteEntryAmount.eq(new BN(47999999))); + assert.ok(user.perpPositions[0].quoteBreakEvenAmount.eq(new BN(47951999))); + assert.ok(user.perpPositions[0].baseAssetAmount.eq(new BN(-48000000000))); + + const market = driftClient.getPerpMarketAccount(0); + assert.ok(market.amm.baseAssetAmountWithAmm.eq(new BN(-48000000000))); + + const orderActionRecord = + eventSubscriber.getEventsArray('OrderActionRecord')[0]; + + assert.ok(orderActionRecord.taker.equals(userAccountPublicKey)); + assert.ok(orderActionRecord.fillRecordId.eq(new BN(5))); + assert.ok(orderActionRecord.baseAssetAmountFilled.eq(new BN(48000000000))); + assert.ok(orderActionRecord.quoteAssetAmountFilled.eq(new BN(47999999))); + assert.ok(orderActionRecord.marketIndex === 0); + }); +}); diff --git a/tests/isolatedPositionLiquidatePerp.ts b/tests/isolatedPositionLiquidatePerp.ts new file mode 100644 index 0000000000..c97feadfe0 --- /dev/null +++ b/tests/isolatedPositionLiquidatePerp.ts @@ -0,0 +1,425 @@ +import * as anchor from '@coral-xyz/anchor'; +import { Program } from '@coral-xyz/anchor'; +import { + BASE_PRECISION, + BN, + ContractTier, + EventSubscriber, + isVariant, + LIQUIDATION_PCT_PRECISION, + OracleGuardRails, + OracleSource, + PositionDirection, + PRICE_PRECISION, + QUOTE_PRECISION, + TestClient, + User, + Wallet, + ZERO, +} from '../sdk/src'; +import { assert } from 'chai'; + +import { Keypair, LAMPORTS_PER_SOL } from '@solana/web3.js'; + +import { + initializeQuoteSpotMarket, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccount, + setFeedPriceNoProgram, +} from './testHelpers'; +import { PERCENTAGE_PRECISION } from '../sdk'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; + +describe('liquidate perp (no open orders)', () => { + const chProgram = anchor.workspace.Drift as Program; + + let driftClient: TestClient; + let eventSubscriber: EventSubscriber; + + let bulkAccountLoader: TestBulkAccountLoader; + + let bankrunContextWrapper: BankrunContextWrapper; + + let usdcMint; + let userUSDCAccount; + + const liquidatorKeyPair = new Keypair(); + let liquidatorUSDCAccount: Keypair; + let liquidatorDriftClient: TestClient; + + // ammInvariant == k == x * y + const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); + const ammInitialQuoteAssetReserve = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetReserve = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + + const usdcAmount = new BN(10 * 10 ** 6); + + before(async () => { + const context = await startAnchor('', [], []); + + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + eventSubscriber = new EventSubscriber( + bankrunContextWrapper.connection.toConnection(), + chProgram + ); + + await eventSubscriber.subscribe(); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + userUSDCAccount = await mockUserUSDCAccount( + usdcMint, + usdcAmount, + bankrunContextWrapper + ); + + const oracle = await mockOracleNoProgram(bankrunContextWrapper, 1); + + driftClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: bankrunContextWrapper.provider.wallet, + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + perpMarketIndexes: [0], + spotMarketIndexes: [0], + subAccountIds: [], + oracleInfos: [ + { + publicKey: oracle, + source: OracleSource.PYTH, + }, + ], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + + await driftClient.initialize(usdcMint.publicKey, true); + await driftClient.subscribe(); + + await driftClient.updateInitialPctToLiquidate( + LIQUIDATION_PCT_PRECISION.toNumber() + ); + + await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); + await driftClient.updatePerpAuctionDuration(new BN(0)); + + const oracleGuardRails: OracleGuardRails = { + priceDivergence: { + markOraclePercentDivergence: PERCENTAGE_PRECISION, + oracleTwap5MinPercentDivergence: PERCENTAGE_PRECISION.muln(100), + }, + validity: { + slotsBeforeStaleForAmm: new BN(100), + slotsBeforeStaleForMargin: new BN(100), + confidenceIntervalMaxSize: new BN(100000), + tooVolatileRatio: new BN(11), // allow 11x change + }, + }; + + await driftClient.updateOracleGuardRails(oracleGuardRails); + + const periodicity = new BN(0); + + await driftClient.initializePerpMarket( + 0, + + oracle, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity + ); + + await driftClient.initializeUserAccountAndDepositCollateral( + usdcAmount, + userUSDCAccount.publicKey + ); + + await driftClient.transferIsolatedPerpPositionDeposit(usdcAmount, 0); + + await driftClient.openPosition( + PositionDirection.LONG, + new BN(175).mul(BASE_PRECISION).div(new BN(10)), // 17.5 SOL + 0, + new BN(0) + ); + + bankrunContextWrapper.fundKeypair(liquidatorKeyPair, LAMPORTS_PER_SOL); + liquidatorUSDCAccount = await mockUserUSDCAccount( + usdcMint, + usdcAmount, + bankrunContextWrapper, + liquidatorKeyPair.publicKey + ); + liquidatorDriftClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new Wallet(liquidatorKeyPair), + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + perpMarketIndexes: [0], + spotMarketIndexes: [0], + subAccountIds: [], + oracleInfos: [ + { + publicKey: oracle, + source: OracleSource.PYTH, + }, + ], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + await liquidatorDriftClient.subscribe(); + + await liquidatorDriftClient.initializeUserAccountAndDepositCollateral( + usdcAmount, + liquidatorUSDCAccount.publicKey + ); + }); + + after(async () => { + await driftClient.unsubscribe(); + await liquidatorDriftClient.unsubscribe(); + await eventSubscriber.unsubscribe(); + }); + + it('liquidate', async () => { + const marketIndex = 0; + + const driftClientUser = new User({ + driftClient: driftClient, + userAccountPublicKey: await driftClient.getUserAccountPublicKey(), + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + await driftClientUser.subscribe(); + + const oracle = driftClient.getPerpMarketAccount(0).amm.oracle; + await setFeedPriceNoProgram(bankrunContextWrapper, 0.9, oracle); + + await driftClient.settlePNL( + driftClientUser.userAccountPublicKey, + driftClientUser.getUserAccount(), + 0 + ); + + await setFeedPriceNoProgram(bankrunContextWrapper, 1.1, oracle); + + await driftClient.settlePNL( + driftClientUser.userAccountPublicKey, + driftClientUser.getUserAccount(), + 0 + ); + + await driftClientUser.unsubscribe(); + + await setFeedPriceNoProgram(bankrunContextWrapper, 0.1, oracle); + + const txSig1 = await liquidatorDriftClient.setUserStatusToBeingLiquidated( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount() + ); + console.log('setUserStatusToBeingLiquidated txSig:', txSig1); + assert(driftClient.getUserAccount().perpPositions[0].positionFlag === 3); + + const txSig = await liquidatorDriftClient.liquidatePerp( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0, + new BN(175).mul(BASE_PRECISION).div(new BN(10)) + ); + + bankrunContextWrapper.connection.printTxLogs(txSig); + + for (let i = 0; i < 32; i++) { + assert(!isVariant(driftClient.getUserAccount().orders[i].status, 'open')); + } + + assert( + liquidatorDriftClient + .getUserAccount() + .perpPositions[0].baseAssetAmount.eq(new BN(17500000000)) + ); + + assert(driftClient.getUserAccount().perpPositions[0].positionFlag === 3); + + const liquidationRecord = + eventSubscriber.getEventsArray('LiquidationRecord')[0]; + assert(liquidationRecord.liquidationId === 1); + assert(isVariant(liquidationRecord.liquidationType, 'liquidatePerp')); + assert(liquidationRecord.liquidatePerp.marketIndex === 0); + assert(liquidationRecord.canceledOrderIds.length === 0); + assert( + liquidationRecord.liquidatePerp.oraclePrice.eq( + PRICE_PRECISION.div(new BN(10)) + ) + ); + assert( + liquidationRecord.liquidatePerp.baseAssetAmount.eq(new BN(-17500000000)) + ); + + assert( + liquidationRecord.liquidatePerp.quoteAssetAmount.eq(new BN(1750000)) + ); + assert(liquidationRecord.liquidatePerp.ifFee.eq(new BN(0))); + assert(liquidationRecord.liquidatePerp.liquidatorFee.eq(new BN(0))); + + const fillRecord = eventSubscriber.getEventsArray('OrderActionRecord')[0]; + assert(isVariant(fillRecord.action, 'fill')); + assert(fillRecord.marketIndex === 0); + assert(isVariant(fillRecord.marketType, 'perp')); + assert(fillRecord.baseAssetAmountFilled.eq(new BN(17500000000))); + assert(fillRecord.quoteAssetAmountFilled.eq(new BN(1750000))); + assert(fillRecord.takerOrderBaseAssetAmount.eq(new BN(17500000000))); + assert( + fillRecord.takerOrderCumulativeBaseAssetAmountFilled.eq( + new BN(17500000000) + ) + ); + assert(fillRecord.takerFee.eq(new BN(0))); + assert(isVariant(fillRecord.takerOrderDirection, 'short')); + assert(fillRecord.makerOrderBaseAssetAmount.eq(new BN(17500000000))); + assert( + fillRecord.makerOrderCumulativeBaseAssetAmountFilled.eq( + new BN(17500000000) + ) + ); + console.log(fillRecord.makerFee.toString()); + assert(fillRecord.makerFee.eq(new BN(ZERO))); + assert(isVariant(fillRecord.makerOrderDirection, 'long')); + + assert(fillRecord.takerExistingQuoteEntryAmount.eq(new BN(17500007))); + assert(fillRecord.takerExistingBaseAssetAmount === null); + assert(fillRecord.makerExistingQuoteEntryAmount === null); + assert(fillRecord.makerExistingBaseAssetAmount === null); + + const _sig2 = await liquidatorDriftClient.liquidatePerpPnlForDeposit( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0, + 0, + driftClient.getUserAccount().perpPositions[0].quoteAssetAmount + ); + + await driftClient.fetchAccounts(); + assert(driftClient.getUserAccount().perpPositions[0].positionFlag === 5); + console.log( + driftClient.getUserAccount().perpPositions[0].quoteAssetAmount.toString() + ); + assert( + driftClient + .getUserAccount() + .perpPositions[0].quoteAssetAmount.eq(new BN(-5767653)) + ); + + await driftClient.updatePerpMarketContractTier(0, ContractTier.A); + const tx1 = await driftClient.updatePerpMarketMaxImbalances( + marketIndex, + new BN(40000).mul(QUOTE_PRECISION), + QUOTE_PRECISION, + QUOTE_PRECISION + ); + bankrunContextWrapper.connection.printTxLogs(tx1); + + await driftClient.fetchAccounts(); + const marketBeforeBankruptcy = + driftClient.getPerpMarketAccount(marketIndex); + assert( + marketBeforeBankruptcy.insuranceClaim.revenueWithdrawSinceLastSettle.eq( + ZERO + ) + ); + assert( + marketBeforeBankruptcy.insuranceClaim.quoteSettledInsurance.eq(ZERO) + ); + assert( + marketBeforeBankruptcy.insuranceClaim.quoteMaxInsurance.eq( + QUOTE_PRECISION + ) + ); + assert(marketBeforeBankruptcy.amm.totalSocialLoss.eq(ZERO)); + const _sig = await liquidatorDriftClient.resolvePerpBankruptcy( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0 + ); + + await driftClient.fetchAccounts(); + // all social loss + const marketAfterBankruptcy = driftClient.getPerpMarketAccount(marketIndex); + assert( + marketAfterBankruptcy.insuranceClaim.revenueWithdrawSinceLastSettle.eq( + ZERO + ) + ); + assert(marketAfterBankruptcy.insuranceClaim.quoteSettledInsurance.eq(ZERO)); + assert( + marketAfterBankruptcy.insuranceClaim.quoteMaxInsurance.eq(QUOTE_PRECISION) + ); + assert(marketAfterBankruptcy.amm.feePool.scaledBalance.eq(ZERO)); + console.log( + 'marketAfterBankruptcy.amm.totalSocialLoss:', + marketAfterBankruptcy.amm.totalSocialLoss.toString() + ); + assert(marketAfterBankruptcy.amm.totalSocialLoss.eq(new BN(5750007))); + + // assert(!driftClient.getUserAccount().isBankrupt); + // assert(!driftClient.getUserAccount().isBeingLiquidated); + assert(driftClient.getUserAccount().perpPositions[0].positionFlag === 1); + + console.log(driftClient.getUserAccount()); + // assert( + // driftClient.getUserAccount().perpPositions[0].quoteAssetAmount.eq(ZERO) + // ); + // assert(driftClient.getUserAccount().perpPositions[0].lpShares.eq(ZERO)); + + const perpBankruptcyRecord = + eventSubscriber.getEventsArray('LiquidationRecord')[0]; + + assert(isVariant(perpBankruptcyRecord.liquidationType, 'perpBankruptcy')); + assert(perpBankruptcyRecord.perpBankruptcy.marketIndex === 0); + console.log(perpBankruptcyRecord.perpBankruptcy.pnl.toString()); + console.log( + perpBankruptcyRecord.perpBankruptcy.cumulativeFundingRateDelta.toString() + ); + assert(perpBankruptcyRecord.perpBankruptcy.pnl.eq(new BN(-5767653))); + console.log( + perpBankruptcyRecord.perpBankruptcy.cumulativeFundingRateDelta.toString() + ); + assert( + perpBankruptcyRecord.perpBankruptcy.cumulativeFundingRateDelta.eq( + new BN(328572000) + ) + ); + + const market = driftClient.getPerpMarketAccount(0); + console.log( + market.amm.cumulativeFundingRateLong.toString(), + market.amm.cumulativeFundingRateShort.toString() + ); + assert(market.amm.cumulativeFundingRateLong.eq(new BN(328580333))); + assert(market.amm.cumulativeFundingRateShort.eq(new BN(-328563667))); + }); +}); diff --git a/tests/isolatedPositionLiquidatePerpwithFill.ts b/tests/isolatedPositionLiquidatePerpwithFill.ts new file mode 100644 index 0000000000..787b5d42f5 --- /dev/null +++ b/tests/isolatedPositionLiquidatePerpwithFill.ts @@ -0,0 +1,338 @@ +import * as anchor from '@coral-xyz/anchor'; +import { Program } from '@coral-xyz/anchor'; +import { + BASE_PRECISION, + BN, + EventSubscriber, + isVariant, + LIQUIDATION_PCT_PRECISION, + OracleGuardRails, + OracleSource, + PositionDirection, + PRICE_PRECISION, + QUOTE_PRECISION, + TestClient, + Wallet, +} from '../sdk/src'; +import { assert } from 'chai'; + +import { Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'; + +import { + createUserWithUSDCAccount, + initializeQuoteSpotMarket, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccount, + setFeedPriceNoProgram, +} from './testHelpers'; +import { OrderType, PERCENTAGE_PRECISION, PerpOperation } from '../sdk'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; + +describe('liquidate perp (no open orders)', () => { + const chProgram = anchor.workspace.Drift as Program; + + let driftClient: TestClient; + let eventSubscriber: EventSubscriber; + + let bulkAccountLoader: TestBulkAccountLoader; + + let bankrunContextWrapper: BankrunContextWrapper; + + let usdcMint; + let userUSDCAccount; + + const liquidatorKeyPair = new Keypair(); + let liquidatorUSDCAccount: Keypair; + let liquidatorDriftClient: TestClient; + + let makerDriftClient: TestClient; + let makerUSDCAccount: PublicKey; + + // ammInvariant == k == x * y + const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); + const ammInitialQuoteAssetReserve = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetReserve = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + + const usdcAmount = new BN(10 * 10 ** 6); + const makerUsdcAmount = new BN(1000 * 10 ** 6); + + let oracle: PublicKey; + + before(async () => { + const context = await startAnchor('', [], []); + + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + eventSubscriber = new EventSubscriber( + bankrunContextWrapper.connection.toConnection(), + //@ts-ignore + chProgram + ); + + await eventSubscriber.subscribe(); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + userUSDCAccount = await mockUserUSDCAccount( + usdcMint, + usdcAmount, + bankrunContextWrapper + ); + + oracle = await mockOracleNoProgram(bankrunContextWrapper, 1); + + driftClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: bankrunContextWrapper.provider.wallet, + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + perpMarketIndexes: [0], + spotMarketIndexes: [0], + subAccountIds: [], + oracleInfos: [ + { + publicKey: oracle, + source: OracleSource.PYTH, + }, + ], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + + await driftClient.initialize(usdcMint.publicKey, true); + await driftClient.subscribe(); + + await driftClient.updateInitialPctToLiquidate( + LIQUIDATION_PCT_PRECISION.toNumber() + ); + + await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); + await driftClient.updatePerpAuctionDuration(new BN(0)); + + const oracleGuardRails: OracleGuardRails = { + priceDivergence: { + markOraclePercentDivergence: PERCENTAGE_PRECISION.muln(100), + oracleTwap5MinPercentDivergence: PERCENTAGE_PRECISION.muln(100), + }, + validity: { + slotsBeforeStaleForAmm: new BN(100), + slotsBeforeStaleForMargin: new BN(100), + confidenceIntervalMaxSize: new BN(100000), + tooVolatileRatio: new BN(11), // allow 11x change + }, + }; + + await driftClient.updateOracleGuardRails(oracleGuardRails); + + const periodicity = new BN(0); + + await driftClient.initializePerpMarket( + 0, + + oracle, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity + ); + + await driftClient.initializeUserAccountAndDepositCollateral( + usdcAmount, + userUSDCAccount.publicKey + ); + + await driftClient.transferIsolatedPerpPositionDeposit(usdcAmount, 0); + + await driftClient.openPosition( + PositionDirection.LONG, + new BN(175).mul(BASE_PRECISION).div(new BN(10)), // 17.5 SOL + 0, + new BN(0) + ); + + bankrunContextWrapper.fundKeypair(liquidatorKeyPair, LAMPORTS_PER_SOL); + liquidatorUSDCAccount = await mockUserUSDCAccount( + usdcMint, + usdcAmount, + bankrunContextWrapper, + liquidatorKeyPair.publicKey + ); + liquidatorDriftClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new Wallet(liquidatorKeyPair), + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + perpMarketIndexes: [0], + spotMarketIndexes: [0], + subAccountIds: [], + oracleInfos: [ + { + publicKey: oracle, + source: OracleSource.PYTH, + }, + ], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + await liquidatorDriftClient.subscribe(); + + await liquidatorDriftClient.initializeUserAccountAndDepositCollateral( + usdcAmount, + liquidatorUSDCAccount.publicKey + ); + + [makerDriftClient, makerUSDCAccount] = await createUserWithUSDCAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + makerUsdcAmount, + [0], + [0], + [ + { + publicKey: oracle, + source: OracleSource.PYTH, + }, + ], + bulkAccountLoader + ); + + await makerDriftClient.deposit(makerUsdcAmount, 0, makerUSDCAccount); + }); + + after(async () => { + await driftClient.unsubscribe(); + await liquidatorDriftClient.unsubscribe(); + await makerDriftClient.unsubscribe(); + await eventSubscriber.unsubscribe(); + }); + + it('liquidate', async () => { + await setFeedPriceNoProgram(bankrunContextWrapper, 0.1, oracle); + await driftClient.updatePerpMarketPausedOperations( + 0, + PerpOperation.AMM_FILL + ); + + try { + const failToPlaceTxSig = await driftClient.placePerpOrder({ + direction: PositionDirection.SHORT, + baseAssetAmount: BASE_PRECISION, + price: PRICE_PRECISION.divn(10), + orderType: OrderType.LIMIT, + reduceOnly: true, + marketIndex: 0, + }); + bankrunContextWrapper.connection.printTxLogs(failToPlaceTxSig); + throw new Error('Expected placePerpOrder to throw an error'); + } catch (error) { + if ( + error.message !== + 'Error processing Instruction 1: custom program error: 0x1773' + ) { + throw new Error(`Unexpected error message: ${error.message}`); + } + } + + await makerDriftClient.placePerpOrder({ + direction: PositionDirection.LONG, + baseAssetAmount: new BN(175).mul(BASE_PRECISION), + price: PRICE_PRECISION.divn(10), + orderType: OrderType.LIMIT, + marketIndex: 0, + }); + + const makerInfos = [ + { + maker: await makerDriftClient.getUserAccountPublicKey(), + makerStats: makerDriftClient.getUserStatsAccountPublicKey(), + makerUserAccount: makerDriftClient.getUserAccount(), + }, + ]; + + const txSig = await liquidatorDriftClient.liquidatePerpWithFill( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0, + makerInfos + ); + + bankrunContextWrapper.connection.printTxLogs(txSig); + + for (let i = 0; i < 32; i++) { + assert(!isVariant(driftClient.getUserAccount().orders[i].status, 'open')); + } + + assert( + liquidatorDriftClient + .getUserAccount() + .perpPositions[0].quoteAssetAmount.eq(new BN(175)) + ); + + assert( + driftClient + .getUserAccount() + .perpPositions[0].baseAssetAmount.eq(new BN(0)) + ); + + assert( + driftClient + .getUserAccount() + .perpPositions[0].quoteAssetAmount.eq(new BN(-15769403)) + ); + + assert( + liquidatorDriftClient.getPerpMarketAccount(0).ifLiquidationFee === 10000 + ); + + assert( + makerDriftClient + .getUserAccount() + .perpPositions[0].baseAssetAmount.eq(new BN(17500000000)) + ); + + assert( + makerDriftClient + .getUserAccount() + .perpPositions[0].quoteAssetAmount.eq(new BN(-1749650)) + ); + + assert( + liquidatorDriftClient.getPerpMarketAccount(0).ifLiquidationFee === 10000 + ); + + await makerDriftClient.liquidatePerpPnlForDeposit( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0, + 0, + QUOTE_PRECISION.muln(20) + ); + + await makerDriftClient.resolvePerpBankruptcy( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0 + ); + }); +});