diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index 0579d0f6de..7b8b8c25c2 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -32,7 +32,7 @@ use crate::state::oracle::OraclePriceData; use crate::state::paused_operations::PerpOperation; use crate::state::perp_market::{PerpMarket, AMM}; use crate::state::spot_market::{SpotBalance, SpotBalanceType, SpotMarket}; -use crate::state::user::{SpotPosition, User}; +use crate::state::user::User; use crate::validate; #[cfg(test)] @@ -596,7 +596,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 { @@ -744,12 +744,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) }; @@ -789,7 +790,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 { @@ -797,7 +798,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()?, @@ -828,14 +829,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..f9155b64a8 100644 --- a/programs/drift/src/controller/amm/tests.rs +++ b/programs/drift/src/controller/amm/tests.rs @@ -5,6 +5,7 @@ use crate::math::constants::{ QUOTE_SPOT_MARKET_INDEX, SPOT_BALANCE_PRECISION, SPOT_CUMULATIVE_INTEREST_PRECISION, }; use crate::state::perp_market::{InsuranceClaim, PoolBalance}; +use crate::state::user::SpotPosition; #[test] fn concentration_coef_tests() { @@ -289,10 +290,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 +304,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 +326,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 +347,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 +387,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 +425,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 +451,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 +476,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 +503,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 +602,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 +625,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 +645,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 +745,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 +768,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 +790,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 +909,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 +948,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 +981,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 +999,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 +1023,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 +1044,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 +1066,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 +1087,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 +1144,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 +1168,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 +1262,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 +1361,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 @@ -1191,6 +1394,7 @@ fn update_pool_balances_revenue_to_fee_new_market() { assert_eq!(spot_market.revenue_pool.scaled_balance, 50000000000); } +#[cfg(test)] mod revenue_pool_transfer_tests { use crate::controller::amm::*; use crate::math::constants::{ @@ -1199,6 +1403,8 @@ mod revenue_pool_transfer_tests { }; use crate::state::perp_market::{InsuranceClaim, PoolBalance}; use crate::state::spot_market::InsuranceFund; + use crate::state::user::SpotPosition; + #[test] fn test_calculate_revenue_pool_transfer() { // Set up input parameters @@ -1512,10 +1718,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 +1734,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 +1753,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 +1772,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 +1839,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 +1855,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 +1874,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..3c2a6e9711 --- /dev/null +++ b/programs/drift/src/controller/isolated_position.rs @@ -0,0 +1,451 @@ +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::math::safe_math::SafeMath; +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::*; + +use super::position::get_position_index; + +#[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, + user_token_amount_after: user.get_total_token_amount(&spot_market)?, + signer: None, + }; + + emit!(deposit_record); + + Ok(()) +} + +pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( + user: &mut User, + user_stats: Option<&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 tvl_before; + { + 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, + )?; + + tvl_before = spot_market.get_tvl()?; + } + + 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); + + if let Some(user_stats) = user_stats { + user.meets_transfer_isolated_position_deposit_margin_requirement( + &perp_market_map, + &spot_market_map, + oracle_map, + MarginRequirementType::Initial, + spot_market_index, + amount as u128, + user_stats, + now, + true, + perp_market_index, + )?; + + 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(); + } + } else { + msg!("Cant transfer isolated position deposit without user stats"); + return Err(ErrorCode::DefaultError); + } + } 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)?; + + // i64::MIN is used to transfer the entire isolated position deposit + let amount = if amount == i64::MIN { + isolated_perp_position_token_amount + } else { + amount.unsigned_abs() as u128 + }; + + validate!( + amount <= 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, + &SpotBalanceType::Deposit, + &mut spot_market, + &mut user.spot_positions[spot_position_index], + false, + None, + )?; + + update_spot_balances( + amount, + &SpotBalanceType::Borrow, + &mut spot_market, + user.force_get_isolated_perp_position_mut(perp_market_index)?, + false, + )?; + + drop(spot_market); + + if let Some(user_stats) = user_stats { + user.meets_transfer_isolated_position_deposit_margin_requirement( + &perp_market_map, + &spot_market_map, + oracle_map, + MarginRequirementType::Initial, + 0, + 0, + user_stats, + now, + false, + perp_market_index, + )?; + + if user.is_isolated_margin_being_liquidated(perp_market_index)? { + user.exit_isolated_margin_liquidation(perp_market_index)?; + } + } else { + if let Ok(_) = get_position_index(&user.perp_positions, perp_market_index) { + msg!("Cant transfer isolated position deposit without user stats if position is still open"); + return Err(ErrorCode::DefaultError); + } + } + } + + user.update_last_active_slot(slot); + + let spot_market = spot_market_map.get_ref(&spot_market_index)?; + + let tvl_after = spot_market.get_tvl()?; + + validate!( + tvl_before.safe_sub(tvl_after)? <= 10, + ErrorCode::DefaultError, + "Transfer Isolated Perp Position Deposit TVL mismatch: before={}, after={}", + tvl_before, + tvl_after + )?; + + 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, + user_token_amount_after: user.get_total_token_amount(&spot_market)?, + signer: 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..2a57899a3c --- /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, + Some(&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, + Some(&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, + Some(&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, + Some(&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, + Some(&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, + Some(&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 4e5543d313..81e7559433 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -1,6 +1,7 @@ use std::ops::{Deref, DerefMut}; use crate::msg; +use crate::state::liquidation_mode::{get_perp_liquidation_mode, LiquidatePerpMode}; use anchor_lang::prelude::*; use crate::controller::amm::get_fee_pool_tokens; @@ -18,7 +19,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, LIQUIDATION_FEE_PRECISION_U128, LIQUIDATION_PCT_PRECISION, @@ -38,7 +39,7 @@ use crate::math::liquidation::{ }; use crate::math::margin::{ calculate_margin_requirement_and_total_collateral_and_liability_info, - calculate_user_safest_position_tiers, meets_initial_margin_requirement, MarginRequirementType, + meets_initial_margin_requirement, MarginRequirementType, }; use crate::math::oracle::DriftAction; use crate::math::orders::{ @@ -95,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", )?; @@ -148,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(()); } @@ -174,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)?; @@ -184,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, @@ -194,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)?; @@ -224,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, @@ -239,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 { @@ -325,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)?; @@ -372,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, @@ -552,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 = @@ -682,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 { @@ -698,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() }); @@ -737,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", )?; @@ -790,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(()); } @@ -806,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)?; @@ -816,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, @@ -826,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)?; @@ -856,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, @@ -871,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 { @@ -941,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)?; @@ -972,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, @@ -1116,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( @@ -1133,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 { @@ -1149,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() }); @@ -1185,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", )?; @@ -1396,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( @@ -1420,6 +1455,7 @@ pub fn liquidate_spot( None, None, None, + true, )?; // check if user exited liquidation territory @@ -1437,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, @@ -1454,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 { @@ -1469,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)?; @@ -1684,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) @@ -1726,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, @@ -1765,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", )?; @@ -1925,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, @@ -1948,6 +1989,7 @@ pub fn liquidate_spot_with_swap_begin( None, None, None, + true, )?; // check if user exited liquidation territory @@ -1965,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) @@ -1981,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 { @@ -1997,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)?; @@ -2210,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::()? @@ -2254,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 { @@ -2273,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, @@ -2313,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", )?; @@ -2418,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)?; @@ -2496,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( @@ -2520,6 +2573,7 @@ pub fn liquidate_borrow_for_perp_pnl( None, None, None, + true, )?; // check if user exited liquidation territory @@ -2533,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; @@ -2553,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 { @@ -2567,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)?; @@ -2713,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 = @@ -2745,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, @@ -2784,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", )?; @@ -2833,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) @@ -2890,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, @@ -2975,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, @@ -2996,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)); @@ -3017,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 { @@ -3050,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 {:?} & {:?}", @@ -3069,7 +3119,7 @@ pub fn liquidate_perp_pnl_for_deposit( intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; if is_contract_tier_violation { @@ -3082,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)?; @@ -3100,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, @@ -3188,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), )?; } @@ -3214,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 = @@ -3238,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, @@ -3256,6 +3307,7 @@ pub fn liquidate_perp_pnl_for_deposit( asset_price, asset_transfer, }, + bit_flags, ..LiquidationRecord::default() }); @@ -3274,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!( @@ -3326,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, @@ -3458,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, @@ -3481,6 +3535,7 @@ pub fn resolve_perp_bankruptcy( clawback_user_payment: None, cumulative_funding_rate_delta, }, + bit_flags, ..LiquidationRecord::default() }); @@ -3499,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!( @@ -3614,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)?; @@ -3648,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( @@ -3658,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) @@ -3696,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 76407a7dad..1e461a5855 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -304,6 +304,10 @@ pub fn place_perp_order( bit_flags = set_order_bit_flag(bit_flags, true, OrderBitFlag::HasBuilder); } + if user.perp_positions[position_index].is_isolated() { + bit_flags = set_order_bit_flag(bit_flags, true, OrderBitFlag::IsIsolatedPosition); + } + let new_order = Order { status: OrderStatus::Open, order_type: params.order_type, @@ -539,8 +543,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; @@ -554,6 +565,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 { @@ -700,6 +715,14 @@ pub fn cancel_order( let (taker, taker_order, maker, maker_order) = get_taker_and_maker_for_order_record(user_key, &user.orders[order_index]); + let mut bit_flags = 0; + if is_perp_order { + let position_index = get_position_index(&user.perp_positions, order_market_index)?; + if user.perp_positions[position_index].is_isolated() { + bit_flags = set_order_bit_flag(bit_flags, true, OrderBitFlag::IsIsolatedPosition); + } + } + let order_action_record = get_order_action_record( now, OrderAction::Cancel, @@ -720,7 +743,7 @@ pub fn cancel_order( maker, maker_order, oracle_map.get_price_data(&oracle_id)?.price, - 0, + bit_flags, None, None, None, @@ -1473,7 +1496,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; } @@ -1968,10 +1991,25 @@ 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); } @@ -2028,11 +2066,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); } @@ -2414,6 +2467,15 @@ pub fn fulfill_perp_order_with_amm( user.orders[order_index].is_signed_msg(), OrderBitFlag::SignedMessage, ); + + if user.perp_positions[position_index].is_isolated() { + order_action_bit_flags = set_order_bit_flag( + order_action_bit_flags, + true, + OrderBitFlag::IsIsolatedPosition, + ); + } + let ( taker_existing_quote_entry_amount, taker_existing_base_asset_amount, @@ -2931,6 +2993,17 @@ pub fn fulfill_perp_order_with_match( taker.orders[taker_order_index].is_signed_msg(), OrderBitFlag::SignedMessage, ); + + if taker.perp_positions[taker_position_index].is_isolated() + || maker.perp_positions[maker_position_index].is_isolated() + { + order_action_bit_flags = set_order_bit_flag( + order_action_bit_flags, + true, + OrderBitFlag::IsIsolatedPosition, + ); + } + let (taker_existing_quote_entry_amount, taker_existing_base_asset_amount) = calculate_existing_position_fields_for_order_action( base_asset_amount_fulfilled_by_maker, @@ -3140,6 +3213,7 @@ pub fn trigger_order( .get_perp_position(market_index)? .worst_case_liability_value(oracle_price, perp_market.contract_type)?; + let mut bit_flags = 0; { update_trigger_order_params( &mut user.orders[order_index], @@ -3164,6 +3238,9 @@ pub fn trigger_order( base_asset_amount, update_open_bids_and_asks, )?; + if user_position.is_isolated() { + bit_flags = set_order_bit_flag(bit_flags, true, OrderBitFlag::IsIsolatedPosition); + } } let is_filler_taker = user_key == filler_key; @@ -3201,7 +3278,7 @@ pub fn trigger_order( None, None, oracle_price, - 0, + bit_flags, None, None, None, @@ -3334,6 +3411,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() { @@ -3360,6 +3440,10 @@ pub fn force_cancel_orders( continue; } + if cross_margin_meets_initial_margin_requirement { + continue; + } + state.spot_fee_structure.flat_filler_fee } MarketType::Perp => { @@ -3374,6 +3458,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 } }; @@ -4182,7 +4278,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 98add9fa67..3aae2e82a4 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -121,8 +121,8 @@ pub fn settle_pnl( } } - let spot_market = &mut spot_market_map.get_quote_spot_market_mut()?; - let perp_market = &mut perp_market_map.get_ref_mut(&market_index)?; + let mut spot_market = spot_market_map.get_quote_spot_market_mut()?; + let mut perp_market = perp_market_map.get_ref_mut(&market_index)?; if perp_market.amm.curve_update_intensity > 0 { let healthy_oracle = perp_market.amm.is_recent_oracle_valid(oracle_map.slot)?; @@ -221,13 +221,13 @@ pub fn settle_pnl( let pnl_pool_token_amount = get_token_amount( perp_market.pnl_pool.scaled_balance, - spot_market, + &spot_market, perp_market.pnl_pool.balance_type(), )?; let fraction_of_fee_pool_token_amount = get_token_amount( perp_market.amm.fee_pool.scaled_balance, - spot_market, + &spot_market, perp_market.amm.fee_pool.balance_type(), )? .safe_div(5)?; @@ -247,10 +247,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(), + &mut perp_market, + &mut spot_market, + user_quote_token_amount, user_unsettled_pnl, now, )?; @@ -292,21 +303,47 @@ 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 + }, + &mut 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 + }, + &mut spot_market, + user.get_quote_spot_position_mut(), + false, + )?; + } update_quote_asset_amount( &mut user.perp_positions[position_index], - perp_market, + &mut perp_market, -pnl_to_settle_with_user.cast()?, )?; @@ -315,10 +352,16 @@ pub fn settle_pnl( let quote_asset_amount_after = user.perp_positions[position_index].quote_asset_amount; let quote_entry_amount = user.perp_positions[position_index].quote_entry_amount; - crate::validation::perp_market::validate_perp_market(perp_market)?; + drop(perp_market); + drop(spot_market); + + let perp_market = perp_market_map.get_ref(&market_index)?; + let spot_market = spot_market_map.get_quote_spot_market()?; + + crate::validation::perp_market::validate_perp_market(&perp_market)?; crate::validation::position::validate_perp_position_with_perp_market( &user.perp_positions[position_index], - perp_market, + &perp_market, )?; emit!(SettlePnlRecord { @@ -405,6 +448,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 db148230f0..a2b5c99486 100644 --- a/programs/drift/src/controller/pnl/delisting.rs +++ b/programs/drift/src/controller/pnl/delisting.rs @@ -2349,7 +2349,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, @@ -2358,7 +2358,6 @@ pub mod delisting_test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -2396,8 +2395,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() @@ -2421,15 +2420,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, @@ -2438,7 +2437,6 @@ pub mod delisting_test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -2504,8 +2502,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(); @@ -2517,7 +2515,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, @@ -2526,7 +2524,6 @@ pub mod delisting_test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -2596,8 +2593,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(); @@ -2609,7 +2606,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, @@ -2618,7 +2615,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..da56517ffa 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}; @@ -1377,7 +1377,7 @@ pub fn user_long_positive_unrealized_pnl_up_to_max_positive_pnl_price_breached() &clock, &state, None, - SettlePnlMode::MustSettle + SettlePnlMode::MustSettle, ) .is_err()); } @@ -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 b16f14d096..8a5de77de1 100644 --- a/programs/drift/src/controller/position/tests.rs +++ b/programs/drift/src/controller/position/tests.rs @@ -109,10 +109,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 b1c19153c1..629ef1e5eb 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 4c5bbcfa91..ea7cc04d87 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -192,8 +192,8 @@ pub enum ErrorCode { SpotMarketInsufficientDeposits, #[msg("UserMustSettleTheirOwnPositiveUnsettledPNL")] UserMustSettleTheirOwnPositiveUnsettledPNL, - #[msg("CantUpdatePoolBalanceType")] - CantUpdatePoolBalanceType, + #[msg("CantUpdateSpotBalanceType")] + CantUpdateSpotBalanceType, #[msg("InsufficientCollateralForSettlingPNL")] InsufficientCollateralForSettlingPNL, #[msg("AMMNotUpdatedInSameSlot")] @@ -694,6 +694,8 @@ pub enum ErrorCode { InvalidLpPoolId, #[msg("MarketIndexNotFoundAmmCache")] MarketIndexNotFoundAmmCache, + #[msg("Invalid Isolated Perp Market")] + InvalidIsolatedPerpMarket, } #[macro_export] diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 3feeabb796..607eaeced7 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -4856,7 +4856,6 @@ pub fn handle_admin_deposit<'c: 'info, 'info>( None, )?; - let user_token_amount_after = spot_position.get_signed_token_amount(&spot_market)?; let token_amount = spot_position.get_token_amount(&spot_market)?; if token_amount == 0 { validate!( @@ -4881,6 +4880,7 @@ pub fn handle_admin_deposit<'c: 'info, 'info>( user.update_last_active_slot(slot); let spot_market = &mut spot_market_map.get_ref_mut(&market_index)?; + let user_token_amount_after = user.get_total_token_amount(spot_market)?; controller::token::receive( &ctx.accounts.token_program, diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index dffdd81d63..6019a45199 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -12,11 +12,13 @@ use solana_program::sysvar::instructions::{ }; use crate::controller::insurance::update_user_stats_if_stake_amount; +use crate::controller::isolated_position::transfer_isolated_perp_position_deposit; use crate::controller::liquidation::{ liquidate_spot_with_swap_begin, liquidate_spot_with_swap_end, }; use crate::controller::orders::cancel_orders; use crate::controller::orders::validate_market_within_price_band; +use crate::controller::position::get_position_index; use crate::controller::position::PositionDirection; use crate::controller::spot_balance::update_spot_balances; use crate::controller::token::{receive, send_from_program_vault}; @@ -473,26 +475,6 @@ pub fn handle_update_user_idle<'c: 'info, 'info>( None, )?; - let mut updated_lp_fields = false; - for perp_position in user.perp_positions.iter_mut() { - if perp_position.lp_shares != 0 - || perp_position.last_base_asset_amount_per_lp != 0 - || perp_position.last_quote_asset_amount_per_lp != 0 - || perp_position.per_lp_base != 0 - { - msg!("Resetting lp fields for perp position {} with lp shares {}, last base asset amount per lp {}, last quote asset amount per lp {}, per lp base {}", perp_position.market_index, perp_position.lp_shares, perp_position.last_base_asset_amount_per_lp, perp_position.last_quote_asset_amount_per_lp, perp_position.per_lp_base); - perp_position.lp_shares = 0; - perp_position.last_base_asset_amount_per_lp = 0; - perp_position.last_quote_asset_amount_per_lp = 0; - perp_position.per_lp_base = 0; - updated_lp_fields = true; - } - } - - if updated_lp_fields { - return Ok(()); - } - let (equity, _) = calculate_user_equity(&user, &perp_market_map, &spot_market_map, &mut oracle_map)?; @@ -671,7 +653,7 @@ pub fn handle_place_signed_msg_taker_order<'c: 'info, 'info>( // TODO: generalize to support multiple market types let AccountMaps { perp_market_map, - spot_market_map, + mut spot_market_map, mut oracle_map, } = load_maps( &mut remaining_accounts, @@ -685,6 +667,7 @@ pub fn handle_place_signed_msg_taker_order<'c: 'info, 'info>( let taker_key = ctx.accounts.user.key(); let mut taker = load_mut!(ctx.accounts.user)?; + let mut taker_stats = load_mut!(ctx.accounts.user_stats)?; let mut signed_msg_taker = ctx.accounts.signed_msg_user_orders.load_mut()?; let escrow = if state.builder_codes_enabled() { @@ -696,11 +679,12 @@ pub fn handle_place_signed_msg_taker_order<'c: 'info, 'info>( place_signed_msg_taker_order( taker_key, &mut taker, + &mut taker_stats, &mut signed_msg_taker, signed_msg_order_params_message_bytes, &ctx.accounts.ix_sysvar.to_account_info(), &perp_market_map, - &spot_market_map, + &mut spot_market_map, &mut oracle_map, high_leverage_mode_config, escrow, @@ -713,11 +697,12 @@ pub fn handle_place_signed_msg_taker_order<'c: 'info, 'info>( pub fn place_signed_msg_taker_order<'c: 'info, 'info>( taker_key: Pubkey, taker: &mut RefMut, + taker_stats: &mut RefMut, signed_msg_account: &mut SignedMsgUserOrdersZeroCopyMut, taker_order_params_message_bytes: Vec, ix_sysvar: &AccountInfo<'info>, perp_market_map: &PerpMarketMap, - spot_market_map: &SpotMarketMap, + spot_market_map: &mut SpotMarketMap, oracle_map: &mut OracleMap, high_leverage_mode_config: Option>, escrow: Option>, @@ -860,10 +845,6 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( return Ok(()); } - if let Some(max_margin_ratio) = verified_message_and_signature.max_margin_ratio { - taker.update_perp_position_max_margin_ratio(market_index, max_margin_ratio)?; - } - // Dont place order if signed msg order already exists let mut taker_order_id_to_use = taker.next_order_id; let mut signed_msg_order_id = @@ -875,6 +856,28 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( return Ok(()); } + if let Some(max_margin_ratio) = verified_message_and_signature.max_margin_ratio { + taker.update_perp_position_max_margin_ratio(market_index, max_margin_ratio)?; + } + + if let Some(isolated_position_deposit) = + verified_message_and_signature.isolated_position_deposit + { + spot_market_map.update_writable_spot_market(0)?; + transfer_isolated_perp_position_deposit( + taker, + Some(taker_stats), + perp_market_map, + spot_market_map, + oracle_map, + clock.slot, + clock.unix_timestamp, + 0, + market_index, + isolated_position_deposit.cast::()?, + )?; + } + // Good to place orders, do stop loss and take profit orders first if let Some(stop_loss_order_params) = verified_message_and_signature.stop_loss_order_params { taker_order_id_to_use += 1; @@ -1092,7 +1095,6 @@ pub fn handle_settle_pnl<'c: 'info, 'info>( )?; let mut remaining_accounts = ctx.remaining_accounts.iter().peekable(); - let AccountMaps { perp_market_map, spot_market_map, @@ -1177,6 +1179,23 @@ pub fn handle_settle_pnl<'c: 'info, 'info>( } } + if let Ok(position_index) = get_position_index(&user.perp_positions, market_index) { + if user.perp_positions[position_index].can_transfer_isolated_position_deposit() { + transfer_isolated_perp_position_deposit( + user, + None, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + clock.slot, + clock.unix_timestamp, + QUOTE_SPOT_MARKET_INDEX, + market_index, + i64::MIN, + )?; + } + } + let spot_market = spot_market_map.get_quote_spot_market()?; validate_spot_market_vault_amount(&spot_market, ctx.accounts.spot_market_vault.amount)?; @@ -1198,7 +1217,6 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( let user = &mut load_mut!(ctx.accounts.user)?; let mut remaining_accounts = ctx.remaining_accounts.iter().peekable(); - let AccountMaps { perp_market_map, spot_market_map, @@ -1290,6 +1308,23 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( } } } + + if let Ok(position_index) = get_position_index(&user.perp_positions, *market_index) { + if user.perp_positions[position_index].can_transfer_isolated_position_deposit() { + transfer_isolated_perp_position_deposit( + user, + None, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + clock.slot, + clock.unix_timestamp, + QUOTE_SPOT_MARKET_INDEX, + *market_index, + i64::MIN, + )?; + } + } } let spot_market = spot_market_map.get_quote_spot_market()?; @@ -3051,23 +3086,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 @@ -3184,6 +3212,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 7811fa2d1e..8e3bf7eb95 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -36,7 +36,7 @@ use crate::instructions::SpotFulfillmentType; use crate::load; use crate::math::casting::Cast; use crate::math::constants::{QUOTE_SPOT_MARKET_INDEX, THIRTEEN_DAY}; -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; use crate::math::margin::meets_initial_margin_requirement; use crate::math::margin::{ @@ -739,7 +739,6 @@ pub fn handle_deposit<'c: 'info, 'info>( None, )?; - let user_token_amount_after = spot_position.get_signed_token_amount(&spot_market)?; let token_amount = spot_position.get_token_amount(&spot_market)?; if token_amount == 0 { validate!( @@ -760,9 +759,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, @@ -771,7 +770,7 @@ pub fn handle_deposit<'c: 'info, 'info>( )?; if !is_being_liquidated { - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); } } @@ -814,6 +813,7 @@ pub fn handle_deposit<'c: 'info, 'info>( } else { None }; + let user_token_amount_after = user.get_total_token_amount(&spot_market)?; let deposit_record = DepositRecord { ts: now, deposit_record_id, @@ -952,8 +952,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); @@ -989,9 +989,7 @@ pub fn handle_withdraw<'c: 'info, 'info>( explanation: deposit_explanation, transfer_user: None, signer: None, - user_token_amount_after: user - .force_get_spot_position_mut(market_index)? - .get_signed_token_amount(&spot_market)?, + user_token_amount_after: user.get_total_token_amount(&spot_market)?, }; emit!(deposit_record); @@ -1135,8 +1133,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); @@ -1163,9 +1161,7 @@ pub fn handle_transfer_deposit<'c: 'info, 'info>( explanation: DepositExplanation::Transfer, transfer_user: Some(to_user_key), signer: None, - user_token_amount_after: from_user - .force_get_spot_position_mut(market_index)? - .get_signed_token_amount(spot_market)?, + user_token_amount_after: from_user.get_total_token_amount(&spot_market)?, }; emit!(deposit_record); } @@ -1190,28 +1186,32 @@ pub fn handle_transfer_deposit<'c: 'info, 'info>( let total_deposits_after = to_user.total_deposits; let total_withdraws_after = to_user.total_withdraws; - let to_spot_position = to_user.force_get_spot_position_mut(spot_market.market_index)?; - - controller::spot_position::update_spot_balances_and_cumulative_deposits( - amount as u128, - &SpotBalanceType::Deposit, - spot_market, - to_spot_position, - false, - None, - )?; - - let token_amount = to_spot_position.get_token_amount(spot_market)?; - if token_amount == 0 { - validate!( - to_spot_position.scaled_balance == 0, - ErrorCode::InvalidSpotPosition, - "deposit left to_user with invalid position. scaled balance = {} token amount = {}", - to_spot_position.scaled_balance, - token_amount + { + let to_spot_position = to_user.force_get_spot_position_mut(spot_market.market_index)?; + + controller::spot_position::update_spot_balances_and_cumulative_deposits( + amount as u128, + &SpotBalanceType::Deposit, + spot_market, + to_spot_position, + false, + None, )?; + + let token_amount = to_spot_position.get_token_amount(spot_market)?; + if token_amount == 0 { + validate!( + to_spot_position.scaled_balance == 0, + ErrorCode::InvalidSpotPosition, + "deposit left to_user with invalid position. scaled balance = {} token amount = {}", + to_spot_position.scaled_balance, + token_amount + )?; + } } + let user_token_amount_after = to_user.get_total_token_amount(&spot_market)?; + let deposit_record_id = get_then_update_id!(spot_market, next_deposit_record_id); let deposit_record = DepositRecord { ts: clock.unix_timestamp, @@ -1231,7 +1231,7 @@ pub fn handle_transfer_deposit<'c: 'info, 'info>( explanation: DepositExplanation::Transfer, transfer_user: Some(from_user_key), signer: None, - user_token_amount_after: to_spot_position.get_signed_token_amount(spot_market)?, + user_token_amount_after, }; emit!(deposit_record); } @@ -1445,9 +1445,7 @@ pub fn handle_transfer_pools<'c: 'info, 'info>( explanation: DepositExplanation::Transfer, transfer_user: Some(to_user_key), signer: None, - user_token_amount_after: from_user - .force_get_spot_position_mut(deposit_from_market_index)? - .get_signed_token_amount(&deposit_from_spot_market)?, + user_token_amount_after: from_user.get_total_token_amount(&deposit_from_spot_market)?, }; emit!(deposit_record); @@ -1483,9 +1481,7 @@ pub fn handle_transfer_pools<'c: 'info, 'info>( explanation: DepositExplanation::Transfer, transfer_user: Some(from_user_key), signer: None, - user_token_amount_after: to_user - .force_get_spot_position_mut(deposit_to_market_index)? - .get_signed_token_amount(&deposit_to_spot_market)?, + user_token_amount_after: to_user.get_total_token_amount(&deposit_to_spot_market)?, }; emit!(deposit_record); } @@ -1553,9 +1549,7 @@ pub fn handle_transfer_pools<'c: 'info, 'info>( explanation: DepositExplanation::Transfer, transfer_user: Some(to_user_key), signer: None, - user_token_amount_after: from_user - .force_get_spot_position_mut(borrow_from_market_index)? - .get_signed_token_amount(&borrow_from_spot_market)?, + user_token_amount_after: from_user.get_total_token_amount(&borrow_from_spot_market)?, }; emit!(deposit_record); @@ -1591,9 +1585,7 @@ pub fn handle_transfer_pools<'c: 'info, 'info>( explanation: DepositExplanation::Transfer, transfer_user: Some(from_user_key), signer: None, - user_token_amount_after: to_user - .force_get_spot_position_mut(borrow_to_market_index)? - .get_signed_token_amount(&borrow_to_spot_market)?, + user_token_amount_after: to_user.get_total_token_amount(&borrow_to_spot_market)?, }; emit!(deposit_record); } @@ -1642,12 +1634,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)?; @@ -1956,14 +1948,16 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( ) }; + let 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!( @@ -1972,14 +1966,16 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( "from user margin requirement is greater than total collateral" )?; + let 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!( @@ -2090,6 +2086,215 @@ 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 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, + Some(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) )] @@ -2289,6 +2494,7 @@ pub fn handle_cancel_orders<'c: 'info, 'info>( market_type, market_index, direction, + false, )?; Ok(()) @@ -4573,6 +4779,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 ae6d90d0ad..df9d1431d2 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -194,6 +194,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 dbe608ceaa..f624b89092 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -196,7 +196,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, @@ -211,7 +211,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) } @@ -227,23 +227,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 fc31624842..66bc74529e 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -31,6 +31,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; @@ -143,8 +145,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 { @@ -221,22 +222,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, )) } @@ -365,7 +354,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); @@ -383,7 +372,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), @@ -436,7 +425,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), @@ -452,8 +441,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); @@ -476,7 +466,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), @@ -513,13 +503,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), @@ -590,22 +582,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, @@ -614,17 +600,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)?; @@ -691,7 +701,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!( @@ -740,27 +750,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(()) } @@ -852,7 +856,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) @@ -1035,6 +1039,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 d4b1eefd2e..3f73436cfd 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -283,7 +283,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, @@ -291,7 +291,6 @@ mod test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -361,7 +360,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, @@ -369,7 +368,6 @@ mod test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -2920,7 +2918,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 ); } @@ -4209,7 +4207,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, @@ -4217,14 +4215,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, @@ -4232,7 +4229,6 @@ mod calculate_perp_position_value_and_pnl_prediction_market { MarginRequirementType::Maintenance, 0, false, - false, ) .unwrap(); @@ -4273,7 +4269,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, @@ -4281,14 +4277,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, @@ -4296,7 +4291,6 @@ mod calculate_perp_position_value_and_pnl_prediction_market { MarginRequirementType::Maintenance, 0, false, - false, ) .unwrap(); @@ -4445,6 +4439,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; @@ -4452,7 +4639,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, @@ -4467,7 +4653,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 7f4844da5a..4ff65bd878 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -794,24 +794,30 @@ pub fn calculate_max_perp_order_size( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, ) -> DriftResult { + let 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 797458fbbc..5d7753f95f 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -242,8 +242,7 @@ pub struct OrderActionRecord { /// precision: PRICE_PRECISION pub oracle_price: i64, - /// Bit flags: - /// 0: is_signed_message + /// Order bit flags, defined in [`crate::state::user::OrderBitFlag`] pub bit_flags: u8, /// precision: QUOTE_PRECISION /// Only Some if the taker reduced position @@ -441,6 +440,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)] @@ -522,6 +522,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..97d7245653 --- /dev/null +++ b/programs/drift/src/state/liquidation_mode.rs @@ -0,0 +1,401 @@ +use solana_program::msg; + +use crate::{ + controller::{ + spot_balance::update_spot_balances, + spot_position::update_spot_balances_and_cumulative_deposits, + }, + error::{DriftResult, ErrorCode}, + math::constants::{LIQUIDATION_PCT_PRECISION, QUOTE_SPOT_MARKET_INDEX}, + 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, +}; + +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 cf2a39ac29..4069a11bf4 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; use crate::math::constants::{ @@ -6,6 +8,7 @@ use crate::math::constants::{ 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; @@ -16,9 +19,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, }, @@ -64,9 +65,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, @@ -115,21 +114,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", @@ -176,7 +160,7 @@ impl MarginContext { } } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub struct MarginCalculation { pub context: MarginContext, pub total_collateral: i128, @@ -189,6 +173,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, @@ -199,13 +184,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 { @@ -214,6 +230,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, @@ -224,7 +241,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, @@ -232,7 +248,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 { @@ -245,7 +261,7 @@ impl MarginCalculation { Ok(()) } - pub fn add_margin_requirement( + pub fn add_cross_margin_margin_requirement( &mut self, margin_requirement: u128, liability_value: u128, @@ -273,10 +289,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(()) } @@ -352,37 +406,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); @@ -391,32 +506,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, @@ -433,15 +592,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, @@ -528,4 +678,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 db5c115036..73b57392f4 100644 --- a/programs/drift/src/state/mod.rs +++ b/programs/drift/src/state/mod.rs @@ -7,6 +7,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 lp_pool; pub mod margin_calculation; diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 36e328037b..bafb04fc5b 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -1062,7 +1062,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/spot_market_map.rs b/programs/drift/src/state/spot_market_map.rs index e93ad8630a..5f1a8726c6 100644 --- a/programs/drift/src/state/spot_market_map.rs +++ b/programs/drift/src/state/spot_market_map.rs @@ -221,6 +221,21 @@ impl<'a> SpotMarketMap<'a> { Ok(spot_market_map) } + + pub fn update_writable_spot_market(&mut self, market_index: u16) -> DriftResult { + if !self.0.contains_key(&market_index) { + return Err(ErrorCode::InvalidSpotMarketAccount); + } + + let account_loader = self.0.get(&market_index).safe_unwrap()?; + if !account_loader.as_ref().is_writable { + return Err(ErrorCode::SpotMarketWrongMutability); + } + + self.1.insert(market_index); + + Ok(()) + } } #[cfg(test)] diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 649c6cfcaa..c4f1af4e30 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -137,10 +137,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 } @@ -259,6 +267,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() @@ -289,6 +331,30 @@ impl User { } } + pub fn get_total_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { + let spot_token_amount = { + if let Ok(spot_position) = self.get_spot_position(spot_market.market_index) { + spot_position.get_signed_token_amount(spot_market)? + } else { + 0_i128 + } + }; + + if spot_market.market_index != QUOTE_SPOT_MARKET_INDEX { + return Ok(spot_token_amount); + } + + let mut perp_token_amount = 0; + for perp_position in self.perp_positions.iter() { + if perp_position.is_isolated() { + perp_token_amount = perp_token_amount + .safe_add(perp_position.get_isolated_token_amount(&spot_market)?)?; + } + } + + spot_token_amount.safe_add(perp_token_amount.cast::()?) + } + pub fn increment_total_deposits( &mut self, amount: u64, @@ -337,34 +403,112 @@ 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::BeingLiquidated as u8); + 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(()) @@ -537,14 +681,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( @@ -592,16 +735,86 @@ 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( + self, + calculation.fuel_deposits, + calculation.fuel_borrows, + calculation.fuel_positions, + now, + )?; + + Ok(true) + } + + pub fn meets_transfer_isolated_position_deposit_margin_requirement( + &mut self, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + margin_requirement_type: MarginRequirementType, + withdraw_market_index: u16, + withdraw_amount: u128, + user_stats: &mut UserStats, + now: i64, + to_isolated_position: bool, + isolated_market_index: u16, + ) -> DriftResult { + let strict = margin_requirement_type == MarginRequirementType::Initial; + let context = MarginContext::standard(margin_requirement_type) + .strict(strict) + .ignore_invalid_deposit_oracles(true) + .fuel_spot_delta(withdraw_market_index, withdraw_amount.cast::()?) + .fuel_numerator(self, now); + + let calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( + self, + perp_market_map, + spot_market_map, + oracle_map, + context, )?; + if calculation.margin_requirement > 0 || calculation.get_num_of_liabilities()? > 0 { + validate!( + calculation.all_liability_oracles_valid, + ErrorCode::InvalidOracle, + "User attempting to withdraw with outstanding liabilities when an oracle is invalid" + )?; + } + + validate_any_isolated_tier_requirements(self, &calculation)?; + + if to_isolated_position { + validate!( + calculation.meets_cross_margin_requirement(), + ErrorCode::InsufficientCollateral, + "margin calculation: {:?}", + calculation + )?; + } else { + // may not exist if user withdrew their remaining deposit + if let Some(isolated_margin_calculation) = calculation + .isolated_margin_calculations + .get(&isolated_market_index) + { + validate!( + isolated_margin_calculation.meets_margin_requirement(), + ErrorCode::InsufficientCollateral, + "margin calculation: {:?}", + calculation + )?; + } + } + user_stats.update_fuel_bonus( self, calculation.fuel_deposits, @@ -981,10 +1194,9 @@ pub struct PerpPosition { /// LP shares allow users to provide liquidity via the AMM /// precision: BASE_PRECISION 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, + /// The scaled balance of the isolated position + /// 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 @@ -996,7 +1208,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 { @@ -1005,7 +1217,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 { @@ -1149,6 +1365,67 @@ 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 + } + + pub fn can_transfer_isolated_position_deposit(&self) -> bool { + self.is_isolated() + && self.isolated_position_scaled_balance > 0 + && !self.is_open_position() + && !self.has_open_order() + && !self.has_unsettled_pnl() + } +} + +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]; @@ -1651,6 +1928,14 @@ pub enum OrderBitFlag { SafeTriggerOrder = 0b00000100, NewTriggerReduceOnly = 0b00001000, HasBuilder = 0b00010000, + IsIsolatedPosition = 0b00100000, +} + +#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] +pub enum PositionFlag { + IsolatedPosition = 0b00000001, + BeingLiquidated = 0b00000010, + Bankrupt = 0b00000100, } #[account(zero_copy(unsafe))] diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 4b2392d1ba..0c819d8aae 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}; diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index eabb861885..2a3fc6bcef 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -2469,6 +2469,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. @@ -4082,6 +4091,191 @@ 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 { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getWithdrawFromIsolatedPerpPositionIx( + amount, + perpMarketIndex, + userTokenAccount, + subAccountId + ), + 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 diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index fcd488872b..9052d789ef 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": [ @@ -14407,13 +14564,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", @@ -14452,8 +14609,8 @@ "type": "u8" }, { - "name": "perLpBase", - "type": "i8" + "name": "positionFlag", + "type": "u8" } ] } @@ -15133,6 +15290,17 @@ ] } }, + { + "name": "LiquidationBitFlag", + "type": { + "kind": "enum", + "variants": [ + { + "name": "IsolatedPosition" + } + ] + } + }, { "name": "SettlePnlExplanation", "type": { @@ -15262,13 +15430,7 @@ "kind": "enum", "variants": [ { - "name": "Standard", - "fields": [ - { - "name": "trackOpenOrdersFraction", - "type": "bool" - } - ] + "name": "Standard" }, { "name": "Liquidation", @@ -15910,6 +16072,23 @@ ] } }, + { + "name": "PositionFlag", + "type": { + "kind": "enum", + "variants": [ + { + "name": "IsolatedPosition" + }, + { + "name": "BeingLiquidated" + }, + { + "name": "Bankrupt" + } + ] + } + }, { "name": "ReferrerStatus", "type": { @@ -16903,6 +17082,11 @@ "defined": "SpotBankruptcyRecord" }, "index": false + }, + { + "name": "bitFlags", + "type": "u8", + "index": false } ] }, @@ -18339,8 +18523,8 @@ }, { "code": 6094, - "name": "CantUpdatePoolBalanceType", - "msg": "CantUpdatePoolBalanceType" + "name": "CantUpdateSpotBalanceType", + "msg": "CantUpdateSpotBalanceType" }, { "code": 6095, @@ -19591,6 +19775,11 @@ "code": 6344, "name": "MarketIndexNotFoundAmmCache", "msg": "MarketIndexNotFoundAmmCache" + }, + { + "code": 6345, + "name": "InvalidIsolatedPerpMarket", + "msg": "Invalid Isolated Perp Market" } ], "metadata": { diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index 19e5ca9625..7b2fceedf2 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -37,6 +37,9 @@ test_files=( highLeverageMode.ts ifRebalance.ts insuranceFundStake.ts + isolatedPositionDriftClient.ts + isolatedPositionLiquidatePerp.ts + isolatedPositionLiquidatePerpwithFill.ts liquidateBorrowForPerpPnl.ts liquidatePerp.ts liquidatePerpWithFill.ts diff --git a/tests/isolatedPositionDriftClient.ts b/tests/isolatedPositionDriftClient.ts new file mode 100644 index 0000000000..0e7ad59b7f --- /dev/null +++ b/tests/isolatedPositionDriftClient.ts @@ -0,0 +1,549 @@ +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(0))); + assert.ok(driftClient.getQuoteAssetTokenAmount().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 () => { + // Re-Deposit USDC, assuming we have 0 balance here + await driftClient.transferIsolatedPerpPositionDeposit(new BN(9855998), 0); + + 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 + ); + }); +}); diff --git a/tests/placeAndMakeSignedMsgBankrun.ts b/tests/placeAndMakeSignedMsgBankrun.ts index 5cf0368d8d..6d1735b220 100644 --- a/tests/placeAndMakeSignedMsgBankrun.ts +++ b/tests/placeAndMakeSignedMsgBankrun.ts @@ -1606,7 +1606,7 @@ describe('place and make signedMsg order', () => { await takerDriftClient.unsubscribe(); }); - it('fills signedMsg with max margin ratio ', async () => { + it('fills signedMsg with max margin ratio and isolated position deposit', async () => { slot = new BN( await bankrunContextWrapper.connection.toConnection().getSlot() ); @@ -1658,6 +1658,7 @@ describe('place and make signedMsg order', () => { stopLossOrderParams: null, takeProfitOrderParams: null, maxMarginRatio: 100, + isolatedPositionDeposit: usdcAmount, }; const signedOrderParams = takerDriftClient.signSignedMsgOrderParamsMessage( @@ -1700,6 +1701,7 @@ describe('place and make signedMsg order', () => { // All orders are placed and one is // @ts-ignore assert(takerPosition.maxMarginRatio === 100); + assert(takerPosition.isolatedPositionScaledBalance.gt(new BN(0))); await takerDriftClientUser.unsubscribe(); await takerDriftClient.unsubscribe(); diff --git a/tests/switchboardTxCus.ts b/tests/switchboardTxCus.ts index 9c46994d99..990542706b 100644 --- a/tests/switchboardTxCus.ts +++ b/tests/switchboardTxCus.ts @@ -219,6 +219,6 @@ describe('switchboard place orders cus', () => { const cus = bankrunContextWrapper.connection.findComputeUnitConsumption(txSig); console.log(cus); - assert(cus < 415000); + assert(cus < 417000); }); });