diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c36ff7e7c..9fa6769db9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +### Fixes + +### Breaking + +## [2.142.0] - 2025-10-14 + +### Features + - program: add titan to whitelisted swap programs ([#1952](https://github.com/drift-labs/protocol-v2/pull/1952)) - program: allow hot wallet to increase max spread and pause funding ([#1957](https://github.com/drift-labs/protocol-v2/pull/1957)) diff --git a/package.json b/package.json index 4b30b26fde..d38f67997c 100644 --- a/package.json +++ b/package.json @@ -95,4 +95,4 @@ "supports-hyperlinks": "<4.1.1", "has-ansi": "<6.0.1" } -} +} \ No newline at end of file diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index e25a578bf6..c9406ae27d 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -595,7 +595,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 { @@ -743,12 +743,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) }; @@ -788,7 +789,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 { @@ -796,7 +797,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()?, @@ -827,14 +828,37 @@ pub fn update_pnl_pool_and_user_balance( return Ok(0); } - let user_spot_position = user.get_quote_spot_position_mut(); + let is_isolated_position = user.get_perp_position(market.market_index)?.is_isolated(); + if is_isolated_position { + let perp_position = user.force_get_isolated_perp_position_mut(market.market_index)?; + let perp_position_token_amount = + perp_position.get_isolated_token_amount(quote_spot_market)?; + + if pnl_to_settle_with_user < 0 { + validate!( + perp_position_token_amount >= pnl_to_settle_with_user.unsigned_abs(), + ErrorCode::InsufficientCollateral, + "user has insufficient deposit for market {}", + market.market_index + )?; + } - transfer_spot_balances( - pnl_to_settle_with_user, - bank, - &mut market.pnl_pool, - user_spot_position, - )?; + transfer_spot_balances( + pnl_to_settle_with_user, + quote_spot_market, + &mut market.pnl_pool, + perp_position, + )?; + } else { + let user_spot_position = user.get_quote_spot_position_mut(); + + transfer_spot_balances( + pnl_to_settle_with_user, + quote_spot_market, + &mut market.pnl_pool, + user_spot_position, + )?; + } Ok(pnl_to_settle_with_user) } diff --git a/programs/drift/src/controller/amm/tests.rs b/programs/drift/src/controller/amm/tests.rs index ba51779f5e..5147d85ef2 100644 --- a/programs/drift/src/controller/amm/tests.rs +++ b/programs/drift/src/controller/amm/tests.rs @@ -289,10 +289,11 @@ fn update_pool_balances_test_high_util_borrow() { let mut spot_position = SpotPosition::default(); let unsettled_pnl = -100; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -302,10 +303,11 @@ fn update_pool_balances_test_high_util_borrow() { // util is low => neg settle ok spot_market.borrow_balance = 0; let unsettled_pnl = -100; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -323,10 +325,12 @@ fn update_pool_balances_test_high_util_borrow() { false, ) .unwrap(); + + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -342,10 +346,12 @@ fn update_pool_balances_test_high_util_borrow() { false, ) .unwrap(); + + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -380,12 +386,26 @@ fn update_pool_balances_test() { let spot_position = SpotPosition::default(); - let to_settle_with_user = - update_pool_balances(&mut market, &mut spot_market, &spot_position, 100, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + let to_settle_with_user = update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 100, + now, + ) + .unwrap(); assert_eq!(to_settle_with_user, 0); - let to_settle_with_user = - update_pool_balances(&mut market, &mut spot_market, &spot_position, -100, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + let to_settle_with_user = update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + -100, + now, + ) + .unwrap(); assert_eq!(to_settle_with_user, -100); assert!(market.amm.fee_pool.balance() > 0); @@ -404,8 +424,15 @@ fn update_pool_balances_test() { assert_eq!(pnl_pool_token_amount, 99); assert_eq!(amm_fee_pool_token_amount, 1); - let to_settle_with_user = - update_pool_balances(&mut market, &mut spot_market, &spot_position, 100, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + let to_settle_with_user = update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 100, + now, + ) + .unwrap(); assert_eq!(to_settle_with_user, 99); let amm_fee_pool_token_amount = get_token_amount( market.amm.fee_pool.balance(), @@ -423,7 +450,15 @@ fn update_pool_balances_test() { assert_eq!(amm_fee_pool_token_amount, 1); market.amm.total_fee_minus_distributions = 0; - update_pool_balances(&mut market, &mut spot_market, &spot_position, -1, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + -1, + now, + ) + .unwrap(); let amm_fee_pool_token_amount = get_token_amount( market.amm.fee_pool.balance(), &spot_market, @@ -440,10 +475,11 @@ fn update_pool_balances_test() { assert_eq!(amm_fee_pool_token_amount, 0); market.amm.total_fee_minus_distributions = 90_000 * QUOTE_PRECISION as i128; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, -(100_000 * QUOTE_PRECISION as i128), now, ) @@ -466,10 +502,11 @@ fn update_pool_balances_test() { // negative fee pool market.amm.total_fee_minus_distributions = -8_008_123_456; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, 1_000_987_789, now, ) @@ -564,7 +601,15 @@ fn update_pool_balances_fee_to_revenue_test() { ); let spot_position = SpotPosition::default(); - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 50000000000000000); // under FEE_POOL_TO_REVENUE_POOL_THRESHOLD assert_eq!(market.pnl_pool.scaled_balance, 50000000000000000); @@ -579,7 +624,15 @@ fn update_pool_balances_fee_to_revenue_test() { let prev_fee_pool_2 = (FEE_POOL_TO_REVENUE_POOL_THRESHOLD + 50 * QUOTE_PRECISION) * SPOT_BALANCE_PRECISION; market.amm.fee_pool.scaled_balance = prev_fee_pool_2; - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.pnl_pool.scaled_balance, 50000000000000000); assert_eq!(market.amm.total_fee_withdrawn, 5000000); @@ -591,12 +644,28 @@ fn update_pool_balances_fee_to_revenue_test() { assert!(spot_market.revenue_pool.scaled_balance > prev_rev_pool); market.insurance_claim.quote_max_insurance = 1; // add min insurance - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.total_fee_withdrawn, 5000001); assert_eq!(spot_market.revenue_pool.scaled_balance, 5000001000000000); market.insurance_claim.quote_max_insurance = 100000000; // add lots of insurance - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.total_fee_withdrawn, 6000000); assert_eq!(spot_market.revenue_pool.scaled_balance, 6000000000000000); } @@ -675,7 +744,15 @@ fn update_pool_balances_fee_to_revenue_low_amm_revenue_test() { ); let spot_position = SpotPosition::default(); - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 50000000000000000); // under FEE_POOL_TO_REVENUE_POOL_THRESHOLD assert_eq!(market.pnl_pool.scaled_balance, 50000000000000000); @@ -690,7 +767,15 @@ fn update_pool_balances_fee_to_revenue_low_amm_revenue_test() { let prev_fee_pool_2 = (FEE_POOL_TO_REVENUE_POOL_THRESHOLD + 50 * QUOTE_PRECISION) * SPOT_BALANCE_PRECISION; market.amm.fee_pool.scaled_balance = prev_fee_pool_2; - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.pnl_pool.scaled_balance, 50000000000000000); assert_eq!(market.amm.total_fee_withdrawn, 1000000); @@ -704,14 +789,30 @@ fn update_pool_balances_fee_to_revenue_low_amm_revenue_test() { market.insurance_claim.quote_max_insurance = 1; // add min insurance market.amm.net_revenue_since_last_funding = 1; - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.total_fee_withdrawn, 1000001); assert_eq!(spot_market.revenue_pool.scaled_balance, 1000001000000000); market.insurance_claim.quote_max_insurance = 100000000; // add lots of insurance market.amm.net_revenue_since_last_funding = 100000000; - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.total_fee_withdrawn, 6000000); assert_eq!(spot_market.revenue_pool.scaled_balance, 6000000000000000); } @@ -807,7 +908,15 @@ fn update_pool_balances_revenue_to_fee_test() { 100 * SPOT_BALANCE_PRECISION ); - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!( market.amm.fee_pool.scaled_balance, @@ -838,7 +947,15 @@ fn update_pool_balances_revenue_to_fee_test() { ); assert_eq!(market.amm.total_fee_minus_distributions, -10000000000); - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!( market.amm.fee_pool.scaled_balance, @@ -863,7 +980,15 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(spot_market_vault_amount, 200000000); // total spot_market deposit balance unchanged during transfers // calling multiple times doesnt effect other than fee pool -> pnl pool - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!( market.amm.fee_pool.scaled_balance, 5 * SPOT_BALANCE_PRECISION @@ -873,7 +998,15 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(market.amm.total_fee_withdrawn, 0); assert_eq!(spot_market.revenue_pool.scaled_balance, 0); - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!( market.amm.fee_pool.scaled_balance, 5 * SPOT_BALANCE_PRECISION @@ -889,7 +1022,15 @@ fn update_pool_balances_revenue_to_fee_test() { let spot_market_backup = spot_market; let market_backup = market; - assert!(update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).is_err()); // assert is_err if any way has revenue pool above deposit balances + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + assert!(update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now + ) + .is_err()); // assert is_err if any way has revenue pool above deposit balances spot_market = spot_market_backup; market = market_backup; spot_market.deposit_balance += 9900000001000; @@ -902,7 +1043,15 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(spot_market.deposit_balance, 10100000001000); assert_eq!(spot_market_vault_amount, 10100000001); - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(spot_market.deposit_balance, 10100000001000); assert_eq!(spot_market.revenue_pool.scaled_balance, 9800000001000); assert_eq!(market.amm.fee_pool.scaled_balance, 105000000000); @@ -916,7 +1065,15 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(market.insurance_claim.last_revenue_withdraw_ts, now); // calling again only does fee -> pnl pool - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 5000000000); assert_eq!(market.pnl_pool.scaled_balance, 295000000000); assert_eq!(market.amm.total_fee_minus_distributions, -9800000000); @@ -929,7 +1086,15 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(market.insurance_claim.last_revenue_withdraw_ts, now); // calling again does nothing - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 5000000000); assert_eq!(market.pnl_pool.scaled_balance, 295000000000); assert_eq!(market.amm.total_fee_minus_distributions, -9800000000); @@ -978,9 +1143,15 @@ fn update_pool_balances_revenue_to_fee_test() { spot_market.revenue_pool.scaled_balance = 9800000001000; let market_backup = market; let spot_market_backup = spot_market; - assert!( - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now + 3600).is_err() - ); // assert is_err if any way has revenue pool above deposit balances + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + assert!(update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now + 3600 + ) + .is_err()); // assert is_err if any way has revenue pool above deposit balances market = market_backup; spot_market = spot_market_backup; spot_market.deposit_balance += 9800000000001; @@ -996,8 +1167,23 @@ fn update_pool_balances_revenue_to_fee_test() { 33928060 + 3600 ); - assert!(update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).is_err()); // now timestamp passed is wrong - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now + 3600).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + assert!(update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now + ) + .is_err()); // now timestamp passed is wrong + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now + 3600, + ) + .unwrap(); assert_eq!(market.insurance_claim.last_revenue_withdraw_ts, 33931660); assert_eq!(spot_market.insurance_fund.last_revenue_settle_ts, 33931660); @@ -1075,7 +1261,15 @@ fn update_pool_balances_revenue_to_fee_devnet_state_test() { let prev_rev_pool = spot_market.revenue_pool.scaled_balance; let prev_tfmd = market.amm.total_fee_minus_distributions; - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 1821000000000); assert_eq!(market.pnl_pool.scaled_balance, 381047000000000); @@ -1166,7 +1360,15 @@ fn update_pool_balances_revenue_to_fee_new_market() { let prev_rev_pool = spot_market.revenue_pool.scaled_balance; // let prev_tfmd = market.amm.total_fee_minus_distributions; - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 50000000000); // $50 @@ -1512,10 +1714,11 @@ mod revenue_pool_transfer_tests { let spot_position = SpotPosition::default(); let unsettled_pnl = -100; let now = 100; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -1527,10 +1730,11 @@ mod revenue_pool_transfer_tests { // revenue pool not yet settled let now = 10000; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -1545,10 +1749,11 @@ mod revenue_pool_transfer_tests { market.amm.net_revenue_since_last_funding = -169; let now = 10000; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -1563,10 +1768,11 @@ mod revenue_pool_transfer_tests { market.amm.net_revenue_since_last_funding = 169; let now = 10000; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -1629,10 +1835,11 @@ mod revenue_pool_transfer_tests { let spot_position = SpotPosition::default(); let unsettled_pnl = -100; let now = 100; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -1644,10 +1851,11 @@ mod revenue_pool_transfer_tests { // revenue pool not yet settled let now = 10000; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -1662,10 +1870,11 @@ mod revenue_pool_transfer_tests { market.amm.net_revenue_since_last_funding = -169; let now = 10000; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) diff --git a/programs/drift/src/controller/isolated_position.rs b/programs/drift/src/controller/isolated_position.rs new file mode 100644 index 0000000000..26059285b7 --- /dev/null +++ b/programs/drift/src/controller/isolated_position.rs @@ -0,0 +1,455 @@ +use std::cell::RefMut; + +use crate::controller; +use crate::controller::spot_balance::update_spot_balances; +use crate::controller::spot_position::update_spot_balances_and_cumulative_deposits; +use crate::error::{DriftResult, ErrorCode}; +use crate::get_then_update_id; +use crate::math::casting::Cast; +use crate::math::liquidation::is_isolated_margin_being_liquidated; +use crate::math::margin::{validate_spot_margin_trading, MarginRequirementType}; +use crate::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, + 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_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + oracle_map, + MarginRequirementType::Initial, + spot_market_index, + amount as u128, + user_stats, + now, + )?; + + validate_spot_margin_trading(user, &perp_market_map, &spot_market_map, oracle_map)?; + + if user.is_cross_margin_being_liquidated() { + user.exit_cross_margin_liquidation(); + } + + if user.is_isolated_margin_being_liquidated(perp_market_index)? { + user.exit_isolated_margin_liquidation(perp_market_index)?; + } + } else { + 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_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + oracle_map, + MarginRequirementType::Initial, + 0, + 0, + user_stats, + now, + )?; + + if user.is_isolated_margin_being_liquidated(perp_market_index)? { + user.exit_isolated_margin_liquidation(perp_market_index)?; + } + + if user.is_cross_margin_being_liquidated() { + user.exit_cross_margin_liquidation(); + } + } 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, + 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..10da79fb5f 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -1,6 +1,9 @@ use std::ops::{Deref, DerefMut}; use crate::msg; +use crate::state::liquidation_mode::{ + get_perp_liquidation_mode, CrossMarginLiquidatePerpMode, LiquidatePerpMode, +}; use anchor_lang::prelude::*; use crate::controller::amm::get_fee_pool_tokens; @@ -18,7 +21,7 @@ use crate::controller::spot_balance::{ }; use crate::controller::spot_position::update_spot_balances_and_cumulative_deposits; use crate::error::{DriftResult, ErrorCode}; -use crate::math::bankruptcy::is_user_bankrupt; +use crate::math::bankruptcy::is_cross_margin_bankrupt; use crate::math::casting::Cast; use crate::math::constants::{ LIQUIDATION_FEE_PRECISION, LIQUIDATION_FEE_PRECISION_U128, LIQUIDATION_PCT_PRECISION, @@ -95,8 +98,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 +153,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 +184,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 +194,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 +206,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 +237,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 +249,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 +339,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 +386,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 +566,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 +697,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 +715,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 +755,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 +810,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 +831,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 +841,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 +853,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 +884,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 +896,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 +970,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 +1001,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 +1145,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 +1163,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 +1181,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 +1218,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 +1429,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 +1457,7 @@ pub fn liquidate_spot( None, None, None, + true, )?; // check if user exited liquidation territory @@ -1437,15 +1475,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 +1492,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 +1507,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 +1722,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 +1765,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 +1804,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 +1964,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 +1991,7 @@ pub fn liquidate_spot_with_swap_begin( None, None, None, + true, )?; // check if user exited liquidation territory @@ -1965,8 +2009,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 +2025,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 +2041,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 +2254,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 +2298,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 +2318,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 +2358,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 +2463,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 +2547,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 +2575,7 @@ pub fn liquidate_borrow_for_perp_pnl( None, None, None, + true, )?; // check if user exited liquidation territory @@ -2533,15 +2589,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 +2609,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 +2623,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 +2769,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 +2802,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 +2841,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 +2892,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 +2943,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 +3014,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 +3042,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 +3064,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 +3101,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 +3121,7 @@ pub fn liquidate_perp_pnl_for_deposit( intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; if is_contract_tier_violation { @@ -3082,7 +3134,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 +3152,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 +3240,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 +3264,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 +3289,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 +3309,7 @@ pub fn liquidate_perp_pnl_for_deposit( asset_price, asset_transfer, }, + bit_flags, ..LiquidationRecord::default() }); @@ -3274,28 +3328,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 +3384,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 +3512,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 +3537,7 @@ pub fn resolve_perp_bankruptcy( clawback_user_payment: None, cumulative_funding_rate_delta, }, + bit_flags, ..LiquidationRecord::default() }); @@ -3499,28 +3556,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 +3671,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 +3705,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 +3716,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 +3758,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 bc8b3aec4c..f91d08c346 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -539,8 +539,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 +561,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 { @@ -1341,6 +1352,20 @@ pub fn fill_perp_order( )? } + if base_asset_amount_after == 0 && user.perp_positions[position_index].open_orders != 0 { + cancel_reduce_only_trigger_orders( + user, + &user_key, + Some(&filler_key), + perp_market_map, + spot_market_map, + oracle_map, + now, + slot, + market_index, + )?; + } + if base_asset_amount == 0 { return Ok((base_asset_amount, quote_asset_amount)); } @@ -1468,7 +1493,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; } @@ -1963,16 +1988,31 @@ fn fulfill_perp_order( )?; if !taker_margin_calculation.meets_margin_requirement() { + let (margin_requirement, total_collateral) = + if taker_margin_calculation.has_isolated_margin_calculation(market_index) { + let isolated_margin_calculation = + taker_margin_calculation.get_isolated_margin_calculation(market_index)?; + ( + isolated_margin_calculation.margin_requirement, + isolated_margin_calculation.total_collateral, + ) + } else { + ( + taker_margin_calculation.margin_requirement, + taker_margin_calculation.total_collateral, + ) + }; + msg!( "taker breached fill requirements (margin requirement {}) (total_collateral {})", - taker_margin_calculation.margin_requirement, - taker_margin_calculation.total_collateral + margin_requirement, + total_collateral ); return Err(ErrorCode::InsufficientCollateral); } } - for (maker_key, maker_base_asset_amount_filled) in maker_fills { + for (maker_key, (maker_base_asset_amount_filled)) in maker_fills { let mut maker = makers_and_referrer.get_ref_mut(&maker_key)?; let maker_stats = if maker.authority == user.authority { @@ -2023,11 +2063,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); } @@ -3019,6 +3074,57 @@ fn get_taker_and_maker_for_order_record( } } +fn cancel_reduce_only_trigger_orders( + user: &mut User, + user_key: &Pubkey, + filler_key: Option<&Pubkey>, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + now: i64, + slot: u64, + perp_market_index: u16, +) -> DriftResult { + for order_index in 0..user.orders.len() { + if user.orders[order_index].status != OrderStatus::Open { + continue; + } + + if user.orders[order_index].market_type != MarketType::Perp { + continue; + } + + if user.orders[order_index].market_index != perp_market_index { + continue; + } + + if !user.orders[order_index].must_be_triggered() { + continue; + } + + if !user.orders[order_index].reduce_only { + continue; + } + + cancel_order( + order_index, + user, + user_key, + perp_market_map, + spot_market_map, + oracle_map, + now, + slot, + OrderActionExplanation::ReduceOnlyOrderIncreasedPosition, + filler_key, + 0, + false, + )?; + } + + Ok(()) +} + pub fn trigger_order( order_id: u32, state: &State, @@ -3329,6 +3435,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() { @@ -3355,6 +3464,10 @@ pub fn force_cancel_orders( continue; } + if cross_margin_meets_initial_margin_requirement { + continue; + } + state.spot_fee_structure.flat_filler_fee } MarketType::Perp => { @@ -3369,6 +3482,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 } }; @@ -4177,7 +4302,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/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index 5a51991cba..3bafe4d39c 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -10570,6 +10570,228 @@ pub mod force_cancel_orders { } } +pub mod cancel_reduce_only_trigger_orders { + use std::str::FromStr; + + use anchor_lang::prelude::Clock; + + use crate::controller::orders::cancel_reduce_only_trigger_orders; + use crate::controller::position::PositionDirection; + use crate::create_account_info; + use crate::create_anchor_account_info; + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I64, LAMPORTS_PER_SOL_I64, PEG_PRECISION, + SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, + SPOT_WEIGHT_PRECISION, + }; + use crate::state::oracle::HistoricalOracleData; + use crate::state::oracle::OracleSource; + use crate::state::perp_market::{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::state::State; + use crate::state::user::{MarketType, OrderStatus, OrderType, SpotPosition, User}; + use crate::test_utils::*; + use crate::test_utils::{ + create_account_info, get_positions, get_pyth_price, get_spot_positions, + }; + + use super::*; + + #[test] + fn test() { + let clock = Clock { + slot: 6, + epoch_start_timestamp: 0, + epoch: 0, + leader_schedule_epoch: 0, + unix_timestamp: 0, + }; + + 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, + terminal_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: 100, + max_fill_reserve_fraction: 100, + order_step_size: 1000, + order_tick_size: 1, + oracle: oracle_price_key, + max_spread: 1000, + base_spread: 0, + long_spread: 0, + short_spread: 0, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: oracle_price.twap, + last_oracle_price_twap_5min: oracle_price.twap, + last_oracle_price: oracle_price.agg.price, + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + status: MarketStatus::Initialized, + ..PerpMarket::default() + }; + market.status = MarketStatus::Active; + market.amm.max_base_asset_reserve = u128::MAX; + market.amm.min_base_asset_reserve = 0; + let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = + crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) + .unwrap(); + let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = + crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) + .unwrap(); + market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; + market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; + market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; + market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + deposit_balance: SPOT_BALANCE_PRECISION, + 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, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + + let mut sol_spot_market = SpotMarket { + market_index: 1, + deposit_balance: SPOT_BALANCE_PRECISION, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + oracle: oracle_price_key, + ..SpotMarket::default_base_market() + }; + create_anchor_account_info!(sol_spot_market, SpotMarket, sol_spot_market_account_info); + + let spot_market_map = SpotMarketMap::load_multiple( + vec![ + &usdc_spot_market_account_info, + &sol_spot_market_account_info, + ], + true, + ) + .unwrap(); + + let mut orders = [Order::default(); 32]; + orders[0] = Order { + market_index: 0, + order_id: 1, + status: OrderStatus::Open, + order_type: OrderType::Limit, + market_type: MarketType::Perp, + ..Order::default() + }; + orders[1] = Order { + market_index: 1, + order_id: 2, + status: OrderStatus::Open, + order_type: OrderType::TriggerMarket, + market_type: MarketType::Perp, + reduce_only: true, + ..Order::default() + }; + orders[2] = Order { + market_index: 0, + order_id: 2, + status: OrderStatus::Open, + order_type: OrderType::TriggerMarket, + market_type: MarketType::Perp, + reduce_only: true, + ..Order::default() + }; + orders[3] = Order { + market_index: 0, + order_id: 2, + status: OrderStatus::Open, + order_type: OrderType::TriggerMarket, + market_type: MarketType::Spot, + reduce_only: true, + ..Order::default() + }; + orders[4] = Order { + market_index: 0, + order_id: 2, + status: OrderStatus::Open, + order_type: OrderType::TriggerLimit, + market_type: MarketType::Perp, + reduce_only: true, + ..Order::default() + }; + + let mut user = User { + authority: Pubkey::from_str("My11111111111111111111111111111111111111111").unwrap(), // different authority than filler + orders, + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: BASE_PRECISION_I64, + open_orders: 2, + open_bids: 100 * BASE_PRECISION_I64, + open_asks: -BASE_PRECISION_I64, + ..PerpPosition::default() + }), + spot_positions: get_spot_positions(SpotPosition { + market_index: 1, + balance_type: SpotBalanceType::Deposit, + scaled_balance: SPOT_BALANCE_PRECISION_U64, + open_orders: 2, + open_bids: 100 * LAMPORTS_PER_SOL_I64, + open_asks: -LAMPORTS_PER_SOL_I64, + ..SpotPosition::default() + }), + ..User::default() + }; + + cancel_reduce_only_trigger_orders( + &mut user, + &Pubkey::default(), + Some(&Pubkey::default()), + &market_map, + &spot_market_map, + &mut oracle_map, + 0, + 0, + 0, + ) + .unwrap(); + + assert_eq!(user.orders[0].status, OrderStatus::Open); + assert_eq!(user.orders[1].status, OrderStatus::Open); + assert_eq!(user.orders[2].status, OrderStatus::Canceled); + assert_eq!(user.orders[3].status, OrderStatus::Open); + assert_eq!(user.orders[4].status, OrderStatus::Canceled); + } +} + pub mod insert_maker_order_info { use crate::controller::orders::insert_maker_order_info; use crate::controller::position::PositionDirection; diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 98add9fa67..faffa1f77c 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -24,6 +24,7 @@ use crate::get_then_update_id; use crate::math::orders::calculate_existing_position_fields_for_order_action; use crate::msg; use crate::state::events::{OrderAction, OrderActionRecord, OrderRecord}; + use crate::state::events::{OrderActionExplanation, SettlePnlExplanation, SettlePnlRecord}; use crate::state::oracle_map::OracleMap; use crate::state::paused_operations::PerpOperation; @@ -33,7 +34,7 @@ use crate::state::settle_pnl_mode::SettlePnlMode; use crate::state::spot_market::{SpotBalance, SpotBalanceType}; use crate::state::spot_market_map::SpotMarketMap; use crate::state::state::State; -use crate::state::user::{MarketType, Order, OrderStatus, OrderType, User}; +use crate::state::user::{MarketType, Order, OrderStatus, OrderType, User, UserStats}; use crate::validate; use anchor_lang::prelude::Pubkey; use anchor_lang::prelude::*; @@ -121,8 +122,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 +222,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 +248,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 +304,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 +353,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 +449,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 390e8502a5..bfba862702 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..828210b4bb 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")] @@ -690,6 +690,8 @@ pub enum ErrorCode { InvalidConstituentOperation, #[msg("Unauthorized for operation")] Unauthorized, + #[msg("Invalid Isolated Perp Market")] + InvalidIsolatedPerpMarket, #[msg("Invalid Lp Pool Id for Operation")] InvalidLpPoolId, #[msg("MarketIndexNotFoundAmmCache")] diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 02219c53e6..9b633ef299 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -4916,6 +4916,7 @@ pub fn handle_admin_deposit<'c: 'info, 'info>( market_index, explanation: DepositExplanation::Reward, transfer_user: None, + signer: Some(ctx.accounts.admin.key()), }; emit!(deposit_record); diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 8706de2b8e..e9ab5d4c21 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}; @@ -651,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, @@ -665,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() { @@ -676,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, @@ -693,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>, @@ -844,6 +849,24 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( 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::()?, + )?; + } + // 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 = @@ -1072,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, @@ -1157,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)?; @@ -1178,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, @@ -1270,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()?; @@ -3031,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 @@ -3164,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/optional_accounts.rs b/programs/drift/src/instructions/optional_accounts.rs index c2365bf0ec..30912a6a07 100644 --- a/programs/drift/src/instructions/optional_accounts.rs +++ b/programs/drift/src/instructions/optional_accounts.rs @@ -8,7 +8,6 @@ use std::convert::TryFrom; use crate::error::ErrorCode::UnableToLoadOracle; use crate::math::safe_unwrap::SafeUnwrap; -use crate::msg; use crate::state::load_ref::load_ref_mut; use crate::state::oracle::PrelaunchOracle; use crate::state::oracle_map::OracleMap; @@ -18,6 +17,7 @@ use crate::state::spot_market_map::SpotMarketMap; use crate::state::state::OracleGuardRails; use crate::state::traits::Size; use crate::state::user::{User, UserStats}; +use crate::{load, load_mut, msg}; use crate::{validate, OracleSource}; use anchor_lang::accounts::account::Account; use anchor_lang::prelude::{AccountInfo, Interface, Pubkey}; diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 6674083742..c3df9a2c36 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -35,7 +35,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::{ @@ -758,9 +758,9 @@ pub fn handle_deposit<'c: 'info, 'info>( } drop(spot_market); - if user.is_being_liquidated() { + if user.is_cross_margin_being_liquidated() { // try to update liquidation status if user is was already being liq'd - let is_being_liquidated = is_user_being_liquidated( + let is_being_liquidated = is_cross_margin_being_liquidated( user, &perp_market_map, &spot_market_map, @@ -769,7 +769,7 @@ pub fn handle_deposit<'c: 'info, 'info>( )?; if !is_being_liquidated { - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); } } @@ -799,6 +799,11 @@ pub fn handle_deposit<'c: 'info, 'info>( } else { DepositExplanation::None }; + let signer = if ctx.accounts.authority.key() != user.authority { + Some(ctx.accounts.authority.key()) + } else { + None + }; let deposit_record = DepositRecord { ts: now, deposit_record_id, @@ -816,6 +821,7 @@ pub fn handle_deposit<'c: 'info, 'info>( market_index, explanation, transfer_user: None, + signer, }; emit!(deposit_record); @@ -935,8 +941,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); @@ -971,6 +977,7 @@ pub fn handle_withdraw<'c: 'info, 'info>( total_withdraws_after: user.total_withdraws, explanation: deposit_explanation, transfer_user: None, + signer: None, }; emit!(deposit_record); @@ -1114,8 +1121,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); @@ -1141,6 +1148,7 @@ pub fn handle_transfer_deposit<'c: 'info, 'info>( total_withdraws_after: from_user.total_withdraws, explanation: DepositExplanation::Transfer, transfer_user: Some(to_user_key), + signer: None, }; emit!(deposit_record); } @@ -1205,6 +1213,7 @@ pub fn handle_transfer_deposit<'c: 'info, 'info>( total_withdraws_after, explanation: DepositExplanation::Transfer, transfer_user: Some(from_user_key), + signer: None, }; emit!(deposit_record); } @@ -1417,6 +1426,7 @@ pub fn handle_transfer_pools<'c: 'info, 'info>( total_withdraws_after: from_user.total_withdraws, explanation: DepositExplanation::Transfer, transfer_user: Some(to_user_key), + signer: None, }; emit!(deposit_record); @@ -1451,6 +1461,7 @@ pub fn handle_transfer_pools<'c: 'info, 'info>( total_withdraws_after: to_user.total_withdraws, explanation: DepositExplanation::Transfer, transfer_user: Some(from_user_key), + signer: None, }; emit!(deposit_record); } @@ -1517,6 +1528,7 @@ pub fn handle_transfer_pools<'c: 'info, 'info>( total_withdraws_after: from_user.total_withdraws, explanation: DepositExplanation::Transfer, transfer_user: Some(to_user_key), + signer: None, }; emit!(deposit_record); @@ -1551,6 +1563,7 @@ pub fn handle_transfer_pools<'c: 'info, 'info>( total_withdraws_after: to_user.total_withdraws, explanation: DepositExplanation::Transfer, transfer_user: Some(from_user_key), + signer: None, }; emit!(deposit_record); } @@ -1599,12 +1612,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)?; @@ -1913,14 +1926,16 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( ) }; + let mut from_user_margin_context = MarginContext::standard(MarginRequirementType::Maintenance) + .fuel_perp_delta(market_index, transfer_amount); + let from_user_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &from_user, &perp_market_map, &spot_market_map, &mut oracle_map, - MarginContext::standard(MarginRequirementType::Maintenance) - .fuel_perp_delta(market_index, transfer_amount), + from_user_margin_context, )?; validate!( @@ -1929,14 +1944,16 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( "from user margin requirement is greater than total collateral" )?; + let mut to_user_margin_context = MarginContext::standard(MarginRequirementType::Initial) + .fuel_perp_delta(market_index, -transfer_amount); + let to_user_margin_requirement = calculate_margin_requirement_and_total_collateral_and_liability_info( &to_user, &perp_market_map, &spot_market_map, &mut oracle_map, - MarginContext::standard(MarginRequirementType::Initial) - .fuel_perp_delta(market_index, -transfer_amount), + to_user_margin_context, )?; validate!( @@ -2047,6 +2064,218 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( Ok(()) } +#[access_control( + deposit_not_paused(&ctx.accounts.state) +)] +pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositIsolatedPerpPosition<'info>>, + spot_market_index: u16, + perp_market_index: u16, + amount: u64, +) -> Result<()> { + let user_key = ctx.accounts.user.key(); + let mut user = load_mut!(ctx.accounts.user)?; + + let state = &ctx.accounts.state; + let clock = Clock::get()?; + let now = clock.unix_timestamp; + let slot = clock.slot; + + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let AccountMaps { + perp_market_map, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts_iter, + &MarketSet::new(), + &get_writable_spot_market_set(spot_market_index), + clock.slot, + Some(state.oracle_guard_rails), + )?; + + let mint = get_token_mint(remaining_accounts_iter)?; + + controller::isolated_position::deposit_into_isolated_perp_position( + user_key, + &mut user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + state, + spot_market_index, + perp_market_index, + amount, + )?; + + let spot_market = spot_market_map.get_ref(&spot_market_index)?; + + controller::token::receive( + &ctx.accounts.token_program, + &ctx.accounts.user_token_account, + &ctx.accounts.spot_market_vault, + &ctx.accounts.authority, + amount, + &mint, + if spot_market.has_transfer_hook() { + Some(remaining_accounts_iter) + } else { + None + }, + )?; + + ctx.accounts.spot_market_vault.reload()?; + + math::spot_withdraw::validate_spot_market_vault_amount( + &spot_market, + ctx.accounts.spot_market_vault.amount, + )?; + + spot_market.validate_max_token_deposits_and_borrows(false)?; + + Ok(()) +} + +#[access_control( + deposit_not_paused(&ctx.accounts.state) + withdraw_not_paused(&ctx.accounts.state) +)] +pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, TransferIsolatedPerpPositionDeposit<'info>>, + spot_market_index: u16, + perp_market_index: u16, + amount: i64, +) -> anchor_lang::Result<()> { + let authority_key = ctx.accounts.authority.key; + let user_key = ctx.accounts.user.key(); + + let state = &ctx.accounts.state; + let clock = Clock::get()?; + let slot = clock.slot; + + let user = &mut load_mut!(ctx.accounts.user)?; + let user_stats = &mut load_mut!(ctx.accounts.user_stats)?; + + let clock = Clock::get()?; + let now = clock.unix_timestamp; + + validate!( + !user.is_bankrupt(), + ErrorCode::UserBankrupt, + "user bankrupt" + )?; + + let AccountMaps { + perp_market_map, + spot_market_map, + mut oracle_map, + } = load_maps( + &mut ctx.remaining_accounts.iter().peekable(), + &MarketSet::new(), + &get_writable_spot_market_set(spot_market_index), + clock.slot, + Some(state.oracle_guard_rails), + )?; + + controller::isolated_position::transfer_isolated_perp_position_deposit( + user, + 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) )] @@ -2246,6 +2475,7 @@ pub fn handle_cancel_orders<'c: 'info, 'info>( market_type, market_index, direction, + false, )?; Ok(()) @@ -3680,6 +3910,7 @@ pub fn handle_begin_swap<'c: 'info, 'info>( )?; } } else { + let mut whitelisted_programs = WHITELISTED_SWAP_PROGRAMS.to_vec(); let mut whitelisted_programs = WHITELISTED_SWAP_PROGRAMS.to_vec(); if !delegate_is_signer { whitelisted_programs.push(AssociatedToken::id()); @@ -4328,10 +4559,7 @@ pub struct InitializeReferrerName<'info> { #[instruction(market_index: u16,)] pub struct Deposit<'info> { pub state: Box>, - #[account( - mut, - constraint = can_sign_for_user(&user, &authority)? - )] + #[account(mut)] pub user: AccountLoader<'info, User>, #[account( mut, @@ -4533,6 +4761,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 cc30448b0b..d710bd0fc7 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..9f759f4822 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, )) } @@ -268,6 +257,7 @@ pub fn calculate_user_safest_position_tiers( Ok((safest_tier_spot_liablity, safest_tier_perp_liablity)) } +// todo make sure everything using this sets isolated_position_market_index correctly pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( user: &User, perp_market_map: &PerpMarketMap, @@ -365,7 +355,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 +373,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 +426,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 +442,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 +467,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 +504,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 +583,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 +601,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 +702,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 +751,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 +857,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 +1040,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..879bfe8321 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 mut margin_context = MarginContext::standard(MarginRequirementType::Initial).strict(true); // calculate initial margin requirement - let MarginCalculation { - margin_requirement, - total_collateral, - .. - } = calculate_margin_requirement_and_total_collateral_and_liability_info( + let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - MarginContext::standard(MarginRequirementType::Initial).strict(true), + margin_context, )?; let user_custom_margin_ratio = user.max_margin_ratio; let perp_position_margin_ratio = user.perp_positions[position_index].max_margin_ratio as u32; let user_high_leverage_mode = user.is_high_leverage_mode(MarginRequirementType::Initial); - let free_collateral_before = total_collateral.safe_sub(margin_requirement.cast()?)?; + let is_isolated_position = user.perp_positions[position_index].is_isolated(); + let free_collateral_before = if is_isolated_position { + margin_calculation + .get_isolated_free_collateral(market_index)? + .cast::()? + } else { + margin_calculation + .get_cross_free_collateral()? + .cast::()? + }; let perp_market = perp_market_map.get_ref(&market_index)?; diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index cdf97002e7..88a97ef815 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -51,6 +51,7 @@ pub struct DepositRecord { pub total_withdraws_after: u64, pub explanation: DepositExplanation, pub transfer_user: Option, + pub signer: Option, } #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Eq, Default)] @@ -438,6 +439,7 @@ pub struct LiquidationRecord { pub liquidate_perp_pnl_for_deposit: LiquidatePerpPnlForDepositRecord, pub perp_bankruptcy: PerpBankruptcyRecord, pub spot_bankruptcy: SpotBankruptcyRecord, + pub bit_flags: u8, } #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Eq, Default)] @@ -519,6 +521,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..4857c0754b --- /dev/null +++ b/programs/drift/src/state/liquidation_mode.rs @@ -0,0 +1,401 @@ +use solana_program::msg; + +use crate::math::constants::{LIQUIDATION_PCT_PRECISION, QUOTE_SPOT_MARKET_INDEX}; +use crate::{ + controller::{ + spot_balance::update_spot_balances, + spot_position::update_spot_balances_and_cumulative_deposits, + }, + error::{DriftResult, ErrorCode}, + math::{ + bankruptcy::{is_cross_margin_bankrupt, is_isolated_margin_bankrupt}, + liquidation::calculate_max_pct_to_liquidate, + margin::calculate_user_safest_position_tiers, + safe_unwrap::SafeUnwrap, + }, + state::margin_calculation::MarginCalculation, + validate, +}; + +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 5a6b441c1c..48e8f26501 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -1061,7 +1061,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 a89467cce3..16e048b08a 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() @@ -337,34 +379,111 @@ impl User { Ok(()) } - pub fn enter_liquidation(&mut self, slot: u64) -> DriftResult { - if self.is_being_liquidated() { + pub fn enter_cross_margin_liquidation(&mut self, slot: u64) -> DriftResult { + if self.is_cross_margin_being_liquidated() { return self.next_liquidation_id.safe_sub(1); } self.add_user_status(UserStatus::BeingLiquidated); self.liquidation_margin_freed = 0; - self.last_active_slot = slot; - Ok(get_then_update_id!(self, next_liquidation_id)) + + let liquidation_id = if self.has_isolated_margin_being_liquidated() { + self.next_liquidation_id.safe_sub(1)? + } else { + self.last_active_slot = slot; + get_then_update_id!(self, next_liquidation_id) + }; + + Ok(liquidation_id) } - pub fn exit_liquidation(&mut self) { + pub fn exit_cross_margin_liquidation(&mut self) { self.remove_user_status(UserStatus::BeingLiquidated); self.remove_user_status(UserStatus::Bankrupt); self.liquidation_margin_freed = 0; } - pub fn enter_bankruptcy(&mut self) { + pub fn enter_cross_margin_bankruptcy(&mut self) { self.remove_user_status(UserStatus::BeingLiquidated); self.add_user_status(UserStatus::Bankrupt); } - pub fn exit_bankruptcy(&mut self) { + pub fn exit_cross_margin_bankruptcy(&mut self) { self.remove_user_status(UserStatus::BeingLiquidated); self.remove_user_status(UserStatus::Bankrupt); self.liquidation_margin_freed = 0; } + pub fn has_isolated_margin_being_liquidated(&self) -> bool { + self.perp_positions + .iter() + .any(|position| position.is_isolated() && position.is_being_liquidated()) + } + + pub fn enter_isolated_margin_liquidation( + &mut self, + perp_market_index: u16, + slot: u64, + ) -> DriftResult { + if self.is_isolated_margin_being_liquidated(perp_market_index)? { + return self.next_liquidation_id.safe_sub(1); + } + + let liquidation_id = if self.is_cross_margin_being_liquidated() + || self.has_isolated_margin_being_liquidated() + { + self.next_liquidation_id.safe_sub(1)? + } else { + self.last_active_slot = slot; + get_then_update_id!(self, next_liquidation_id) + }; + + let perp_position = self.force_get_isolated_perp_position_mut(perp_market_index)?; + + perp_position.position_flag |= PositionFlag::BeingLiquidated as u8; + + Ok(liquidation_id) + } + + pub fn exit_isolated_margin_liquidation(&mut self, perp_market_index: u16) -> DriftResult { + let perp_position = self.force_get_isolated_perp_position_mut(perp_market_index)?; + perp_position.position_flag &= !(PositionFlag::BeingLiquidated as u8); + perp_position.position_flag &= !(PositionFlag::Bankrupt as u8); + Ok(()) + } + + pub fn is_isolated_margin_being_liquidated(&self, perp_market_index: u16) -> DriftResult { + if let Ok(perp_position) = self.get_isolated_perp_position(perp_market_index) { + Ok(perp_position.is_being_liquidated()) + } else { + Ok(false) + } + } + + pub fn has_isolated_margin_bankrupt(&self) -> bool { + self.perp_positions + .iter() + .any(|position| position.is_isolated() && position.is_bankrupt()) + } + + pub fn enter_isolated_margin_bankruptcy(&mut self, perp_market_index: u16) -> DriftResult { + let perp_position = self.force_get_isolated_perp_position_mut(perp_market_index)?; + perp_position.position_flag &= !(PositionFlag::BeingLiquidated as u8); + perp_position.position_flag |= PositionFlag::Bankrupt as u8; + Ok(()) + } + + pub fn exit_isolated_margin_bankruptcy(&mut self, perp_market_index: u16) -> DriftResult { + let perp_position = self.force_get_isolated_perp_position_mut(perp_market_index)?; + perp_position.position_flag &= !(PositionFlag::Bankrupt as u8); + Ok(()) + } + + pub fn is_isolated_margin_bankrupt(&self, perp_market_index: u16) -> DriftResult { + let perp_position = self.get_isolated_perp_position(perp_market_index)?; + Ok(perp_position.position_flag & (PositionFlag::Bankrupt as u8) != 0) + } + pub fn increment_margin_freed(&mut self, margin_free: u64) -> DriftResult { self.liquidation_margin_freed = self.liquidation_margin_freed.safe_add(margin_free)?; Ok(()) @@ -537,14 +656,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,14 +710,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( @@ -975,8 +1092,8 @@ pub struct PerpPosition { pub lp_shares: u64, /// The last base asset amount per lp the amm had /// Used to settle the users lp position - /// precision: BASE_PRECISION - pub last_base_asset_amount_per_lp: i64, + /// precision: SPOT_BALANCE_PRECISION + pub isolated_position_scaled_balance: u64, /// The last quote asset amount per lp the amm had /// Used to settle the users lp position /// precision: QUOTE_PRECISION @@ -988,7 +1105,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 { @@ -997,7 +1114,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 { @@ -1141,6 +1262,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]; @@ -1640,6 +1822,13 @@ pub enum OrderBitFlag { HasBuilder = 0b00010000, } +#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] +pub enum PositionFlag { + IsolatedPosition = 0b00000001, + BeingLiquidated = 0b00000010, + Bankrupt = 0b00000100, +} + #[account(zero_copy(unsafe))] #[derive(Eq, PartialEq, Debug)] #[repr(C)] diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 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/programs/drift/src/validation/sig_verification/tests.rs b/programs/drift/src/validation/sig_verification/tests.rs index 250d5c871d..13335acc18 100644 --- a/programs/drift/src/validation/sig_verification/tests.rs +++ b/programs/drift/src/validation/sig_verification/tests.rs @@ -197,6 +197,55 @@ mod sig_verification { assert_eq!(order_params.auction_end_price, Some(237000000i64)); } + #[test] + fn test_deserialize_into_verified_message_non_delegate_with_isolated_position_deposit() { + let signature = [1u8; 64]; + let payload = vec![ + 200, 213, 166, 94, 34, 52, 245, 93, 0, 1, 0, 3, 0, 96, 254, 205, 0, 0, 0, 0, 64, 85, + 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 128, 133, 181, 13, 0, 0, 0, 0, + 1, 64, 85, 32, 14, 0, 0, 0, 0, 2, 0, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, + 71, 49, 1, 0, 28, 78, 14, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 1, 64, 58, 105, 13, + 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 0, 0, 0, 1, 1, + ]; + + // Test deserialization with delegate signer + let result = deserialize_into_verified_message(payload, &signature, false); + assert!(result.is_ok()); + + let verified_message = result.unwrap(); + + // Verify the deserialized message has expected structure + assert_eq!(verified_message.signature, signature); + assert_eq!(verified_message.sub_account_id, Some(2)); + assert_eq!(verified_message.delegate_signed_taker_pubkey, None); + assert_eq!(verified_message.slot, 2345); + assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); + assert!(verified_message.isolated_position_deposit.is_some()); + assert_eq!(verified_message.isolated_position_deposit.unwrap(), 1); + + assert!(verified_message.take_profit_order_params.is_some()); + let tp = verified_message.take_profit_order_params.unwrap(); + assert_eq!(tp.base_asset_amount, 3456000000u64); + assert_eq!(tp.trigger_price, 240000000u64); + + assert!(verified_message.stop_loss_order_params.is_some()); + let sl = verified_message.stop_loss_order_params.unwrap(); + assert_eq!(sl.base_asset_amount, 3456000000u64); + assert_eq!(sl.trigger_price, 225000000u64); + + // Verify order params + let order_params = &verified_message.signed_msg_order_params; + assert_eq!(order_params.user_order_id, 3); + assert_eq!(order_params.direction, PositionDirection::Long); + assert_eq!(order_params.base_asset_amount, 3456000000u64); + assert_eq!(order_params.price, 237000000u64); + assert_eq!(order_params.market_index, 0); + assert_eq!(order_params.reduce_only, false); + assert_eq!(order_params.auction_duration, Some(10)); + assert_eq!(order_params.auction_start_price, Some(230000000i64)); + assert_eq!(order_params.auction_end_price, Some(237000000i64)); + } + #[test] fn test_deserialize_into_verified_message_delegate() { let signature = [1u8; 64]; diff --git a/sdk/VERSION b/sdk/VERSION index f60efa1527..98b45d6951 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.148.0-beta.1 \ No newline at end of file +2.148.0-beta.1 diff --git a/sdk/package.json b/sdk/package.json index f7d68852d8..430543b02c 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -146,4 +146,4 @@ "@grpc/grpc-js": false, "zstddec": false } -} +} \ No newline at end of file diff --git a/sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts b/sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts index 31a68ce02e..fa597afdbf 100644 --- a/sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts +++ b/sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts @@ -27,7 +27,7 @@ type ProgramAccountSubscriptionAsyncIterable = AsyncIterable< rentEpoch: bigint; space: bigint; }> & - Readonly; + Readonly; pubkey: Address; }>; }> @@ -93,8 +93,7 @@ type ProgramAccountSubscriptionAsyncIterable = AsyncIterable< */ export class WebSocketProgramAccountsSubscriberV2 - implements ProgramAccountSubscriber -{ + implements ProgramAccountSubscriber { subscriptionName: string; accountDiscriminator: string; bufferAndSlotMap: Map = new Map(); @@ -311,8 +310,7 @@ export class WebSocketProgramAccountsSubscriberV2 } const endTime = performance.now(); console.log( - `[PROFILING] ${this.subscriptionName}.subscribe() completed in ${ - endTime - startTime + `[PROFILING] ${this.subscriptionName}.subscribe() completed in ${endTime - startTime }ms` ); } @@ -867,8 +865,7 @@ export class WebSocketProgramAccountsSubscriberV2 if (this.missedChangeDetected) { if (this.resubOpts?.logResubMessages) { console.log( - `[${this.subscriptionName}] Missed change detected for ${ - this.accountsWithMissedUpdates.size + `[${this.subscriptionName}] Missed change detected for ${this.accountsWithMissedUpdates.size } accounts: ${Array.from(this.accountsWithMissedUpdates).join( ', ' )}, resubscribing` diff --git a/sdk/src/constituentMap/constituentMap.ts b/sdk/src/constituentMap/constituentMap.ts index 91b7479a60..244fff575e 100644 --- a/sdk/src/constituentMap/constituentMap.ts +++ b/sdk/src/constituentMap/constituentMap.ts @@ -20,17 +20,17 @@ export type ConstituentMapConfig = { driftClient: DriftClient; connection?: Connection; subscriptionConfig: - | { - type: 'polling'; - frequency: number; - commitment?: Commitment; - } - | { - type: 'websocket'; - resubTimeoutMs?: number; - logResubMessages?: boolean; - commitment?: Commitment; - }; + | { + type: 'polling'; + frequency: number; + commitment?: Commitment; + } + | { + type: 'websocket'; + resubTimeoutMs?: number; + logResubMessages?: boolean; + commitment?: Commitment; + }; lpPoolId?: number; // potentially use these to filter Constituent accounts additionalFilters?: MemcmpFilter[]; diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index d780f6cad2..9659596148 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -372,8 +372,8 @@ export class DriftClient { this.authoritySubAccountMap = config.authoritySubAccountMap ? config.authoritySubAccountMap : config.subAccountIds - ? new Map([[this.authority.toString(), config.subAccountIds]]) - : new Map(); + ? new Map([[this.authority.toString(), config.subAccountIds]]) + : new Map(); this.includeDelegates = config.includeDelegates ?? false; if (config.accountSubscription?.type === 'polling') { @@ -406,8 +406,6 @@ export class DriftClient { resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, logResubMessages: config.accountSubscription?.logResubMessages, commitment: config.accountSubscription?.commitment, - programUserAccountSubscriber: - config.accountSubscription?.programUserAccountSubscriber, }; this.userStatsAccountSubscriptionConfig = { type: 'websocket', @@ -476,10 +474,7 @@ export class DriftClient { } ); } else { - const accountSubscriberClass = - config.accountSubscription?.driftClientAccountSubscriber ?? - WebSocketDriftClientAccountSubscriber; - this.accountSubscriber = new accountSubscriberClass( + this.accountSubscriber = new WebSocketDriftClientAccountSubscriber( this.program, config.perpMarketIndexes ?? [], config.spotMarketIndexes ?? [], @@ -863,8 +858,8 @@ export class DriftClient { this.authoritySubAccountMap = authoritySubaccountMap ? authoritySubaccountMap : subAccountIds - ? new Map([[this.authority.toString(), subAccountIds]]) - : new Map(); + ? new Map([[this.authority.toString(), subAccountIds]]) + : new Map(); /* Reset user stats account */ if (this.userStats?.isSubscribed) { @@ -1016,7 +1011,7 @@ export class DriftClient { [...this.authoritySubAccountMap.values()][0][0] ?? 0, new PublicKey( [...this.authoritySubAccountMap.keys()][0] ?? - this.authority.toString() + this.authority.toString() ) ); } @@ -2469,6 +2464,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. @@ -3346,19 +3350,19 @@ export class DriftClient { const depositCollateralIx = isFromSubaccount ? await this.getTransferDepositIx( - amount, - marketIndex, - fromSubAccountId, - subAccountId - ) + amount, + marketIndex, + fromSubAccountId, + subAccountId + ) : await this.getDepositInstruction( - amount, - marketIndex, - userTokenAccount, - subAccountId, - false, - false - ); + amount, + marketIndex, + userTokenAccount, + subAccountId, + false, + false + ); if (subAccountId === 0) { if ( @@ -4068,6 +4072,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 @@ -4398,14 +4587,14 @@ export class DriftClient { const marketOrderTxIxs = positionMaxLev ? this.getPlaceOrdersAndSetPositionMaxLevIx( - [orderParams, ...bracketOrdersParams], - positionMaxLev, - userAccount.subAccountId - ) + [orderParams, ...bracketOrdersParams], + positionMaxLev, + userAccount.subAccountId + ) : this.getPlaceOrdersIx( - [orderParams, ...bracketOrdersParams], - userAccount.subAccountId - ); + [orderParams, ...bracketOrdersParams], + userAccount.subAccountId + ); ixPromisesForTxs.marketOrderTx = marketOrderTxIxs; @@ -4553,10 +4742,10 @@ export class DriftClient { const user = isDepositToTradeTx ? getUserAccountPublicKeySync( - this.program.programId, - this.authority, - subAccountId - ) + this.program.programId, + this.authority, + subAccountId + ) : await this.getUserAccountPublicKey(subAccountId); const remainingAccounts = this.getRemainingAccounts({ @@ -5193,14 +5382,14 @@ export class DriftClient { const marketIndex = order ? order.marketIndex : userAccount.orders.find( - (order) => order.orderId === userAccount.nextOrderId - 1 - ).marketIndex; + (order) => order.orderId === userAccount.nextOrderId - 1 + ).marketIndex; makerInfo = Array.isArray(makerInfo) ? makerInfo : makerInfo - ? [makerInfo] - : []; + ? [makerInfo] + : []; const userAccounts = [userAccount]; for (const maker of makerInfo) { @@ -5416,14 +5605,14 @@ export class DriftClient { const marketIndex = order ? order.marketIndex : userAccount.orders.find( - (order) => order.orderId === userAccount.nextOrderId - 1 - ).marketIndex; + (order) => order.orderId === userAccount.nextOrderId - 1 + ).marketIndex; makerInfo = Array.isArray(makerInfo) ? makerInfo : makerInfo - ? [makerInfo] - : []; + ? [makerInfo] + : []; const userAccounts = [userAccount]; for (const maker of makerInfo) { @@ -7061,8 +7250,8 @@ export class DriftClient { makerInfo = Array.isArray(makerInfo) ? makerInfo : makerInfo - ? [makerInfo] - : []; + ? [makerInfo] + : []; const userAccounts = [this.getUserAccount(subAccountId)]; for (const maker of makerInfo) { @@ -7311,13 +7500,13 @@ export class DriftClient { prefix, delegateSigner ? this.program.coder.types.encode( - 'SignedMsgOrderParamsDelegateMessage', - withBuilderDefaults as SignedMsgOrderParamsDelegateMessage - ) + 'SignedMsgOrderParamsDelegateMessage', + withBuilderDefaults as SignedMsgOrderParamsDelegateMessage + ) : this.program.coder.types.encode( - 'SignedMsgOrderParamsMessage', - withBuilderDefaults as SignedMsgOrderParamsMessage - ), + 'SignedMsgOrderParamsMessage', + withBuilderDefaults as SignedMsgOrderParamsMessage + ), ]); return buf; } @@ -7405,17 +7594,13 @@ export class DriftClient { isDelegateSigner ); - const writableSpotMarketIndexes = signedMessage.isolatedPositionDeposit?.gt( - ZERO - ) - ? [QUOTE_SPOT_MARKET_INDEX] - : undefined; - const remainingAccounts = this.getRemainingAccounts({ userAccounts: [takerInfo.takerUserAccount], useMarketLastSlotCache: false, readablePerpMarketIndex: marketIndex, - writableSpotMarketIndexes, + writableSpotMarketIndexes: signedMessage.isolatedPositionDeposit?.gt(new BN(0)) + ? [QUOTE_SPOT_MARKET_INDEX] + : undefined, }); if (isUpdateHighLeverageMode(signedMessage.signedMsgOrderParams.bitFlags)) { @@ -11058,8 +11243,8 @@ export class DriftClient { ): Promise { const remainingAccounts = userAccount ? this.getRemainingAccounts({ - userAccounts: [userAccount], - }) + userAccounts: [userAccount], + }) : undefined; const ix = await this.program.instruction.disableUserHighLeverageMode( diff --git a/sdk/src/driftClientConfig.ts b/sdk/src/driftClientConfig.ts index 4ca3a8a9eb..ca1971a39d 100644 --- a/sdk/src/driftClientConfig.ts +++ b/sdk/src/driftClientConfig.ts @@ -5,7 +5,7 @@ import { PublicKey, TransactionVersion, } from '@solana/web3.js'; -import { IWallet, TxParams, UserAccount } from './types'; +import { IWallet, TxParams } from './types'; import { OracleInfo } from './oracles/types'; import { BulkAccountLoader } from './accounts/bulkAccountLoader'; import { DriftEnv } from './config'; @@ -59,50 +59,46 @@ export type DriftClientConfig = { export type DriftClientSubscriptionConfig = | { - type: 'grpc'; - grpcConfigs: GrpcConfigs; - resubTimeoutMs?: number; - logResubMessages?: boolean; - driftClientAccountSubscriber?: new ( - grpcConfigs: GrpcConfigs, - program: Program, - perpMarketIndexes: number[], - spotMarketIndexes: number[], - oracleInfos: OracleInfo[], - shouldFindAllMarketsAndOracles: boolean, - delistedMarketSetting: DelistedMarketSetting - ) => - | grpcDriftClientAccountSubscriberV2 - | grpcDriftClientAccountSubscriber; - grpcMultiUserAccountSubscriber?: grpcMultiUserAccountSubscriber; - } + type: 'grpc'; + grpcConfigs: GrpcConfigs; + resubTimeoutMs?: number; + logResubMessages?: boolean; + driftClientAccountSubscriber?: new ( + grpcConfigs: GrpcConfigs, + program: Program, + perpMarketIndexes: number[], + spotMarketIndexes: number[], + oracleInfos: OracleInfo[], + shouldFindAllMarketsAndOracles: boolean, + delistedMarketSetting: DelistedMarketSetting + ) => + | grpcDriftClientAccountSubscriberV2 + | grpcDriftClientAccountSubscriber; + grpcMultiUserAccountSubscriber?: grpcMultiUserAccountSubscriber; + } | { - type: 'websocket'; - resubTimeoutMs?: number; - logResubMessages?: boolean; - commitment?: Commitment; - programUserAccountSubscriber?: WebSocketProgramAccountSubscriber; - perpMarketAccountSubscriber?: new ( - accountName: string, - program: Program, - accountPublicKey: PublicKey, - decodeBuffer?: (buffer: Buffer) => any, - resubOpts?: ResubOpts, - commitment?: Commitment - ) => WebSocketAccountSubscriberV2 | WebSocketAccountSubscriber; - /** If you use V2 here, whatever you pass for perpMarketAccountSubscriber will be ignored and it will use v2 under the hood regardless */ - driftClientAccountSubscriber?: new ( - program: Program, - perpMarketIndexes: number[], - spotMarketIndexes: number[], - oracleInfos: OracleInfo[], - shouldFindAllMarketsAndOracles: boolean, - delistedMarketSetting: DelistedMarketSetting - ) => - | WebSocketDriftClientAccountSubscriber - | WebSocketDriftClientAccountSubscriberV2; - } + type: 'websocket'; + resubTimeoutMs?: number; + logResubMessages?: boolean; + commitment?: Commitment; + perpMarketAccountSubscriber?: new ( + accountName: string, + program: Program, + accountPublicKey: PublicKey, + decodeBuffer?: (buffer: Buffer) => any, + resubOpts?: ResubOpts, + commitment?: Commitment + ) => WebSocketAccountSubscriberV2 | WebSocketAccountSubscriber; + /** If you use V2 here, whatever you pass for perpMarketAccountSubscriber will be ignored and it will use v2 under the hood regardless */ + driftClientAccountSubscriber?: new ( + program: Program, + accountPublicKey: PublicKey, + decodeBuffer?: (buffer: Buffer) => any, + resubOpts?: ResubOpts, + commitment?: Commitment + ) => WebSocketAccountSubscriberV2 | WebSocketAccountSubscriber; + } | { - type: 'polling'; - accountLoader: BulkAccountLoader; - }; + type: 'polling'; + accountLoader: BulkAccountLoader; + }; diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 56b20bc146..01fc2ddef1 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": [ @@ -14412,13 +14569,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", @@ -14457,8 +14614,8 @@ "type": "u8" }, { - "name": "perLpBase", - "type": "i8" + "name": "positionFlag", + "type": "u8" } ] } @@ -15138,6 +15295,17 @@ ] } }, + { + "name": "LiquidationBitFlag", + "type": { + "kind": "enum", + "variants": [ + { + "name": "IsolatedPosition" + } + ] + } + }, { "name": "SettlePnlExplanation", "type": { @@ -15267,13 +15435,7 @@ "kind": "enum", "variants": [ { - "name": "Standard", - "fields": [ - { - "name": "trackOpenOrdersFraction", - "type": "bool" - } - ] + "name": "Standard" }, { "name": "Liquidation", @@ -15912,6 +16074,23 @@ ] } }, + { + "name": "PositionFlag", + "type": { + "kind": "enum", + "variants": [ + { + "name": "IsolatedPosition" + }, + { + "name": "BeingLiquidated" + }, + { + "name": "Bankrupt" + } + ] + } + }, { "name": "ReferrerStatus", "type": { @@ -16131,6 +16310,13 @@ "option": "publicKey" }, "index": false + }, + { + "name": "signer", + "type": { + "option": "publicKey" + }, + "index": false } ] }, @@ -16876,6 +17062,11 @@ "defined": "SpotBankruptcyRecord" }, "index": false + }, + { + "name": "bitFlags", + "type": "u8", + "index": false } ] }, @@ -18312,8 +18503,8 @@ }, { "code": 6094, - "name": "CantUpdatePoolBalanceType", - "msg": "CantUpdatePoolBalanceType" + "name": "CantUpdateSpotBalanceType", + "msg": "CantUpdateSpotBalanceType" }, { "code": 6095, @@ -19557,6 +19748,11 @@ }, { "code": 6343, + "name": "InvalidIsolatedPerpMarket", + "msg": "Invalid Isolated Perp Market" + }, + { + "code": 6344, "name": "InvalidLpPoolId", "msg": "Invalid Lp Pool Id for Operation" }, diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 6c73a0de63..b6f69eeb3a 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -12,10 +12,6 @@ export * from './accounts/webSocketDriftClientAccountSubscriber'; export * from './accounts/webSocketInsuranceFundStakeAccountSubscriber'; export * from './accounts/webSocketHighLeverageModeConfigAccountSubscriber'; export { WebSocketAccountSubscriberV2 } from './accounts/webSocketAccountSubscriberV2'; -export { WebSocketProgramAccountSubscriber } from './accounts/webSocketProgramAccountSubscriber'; -export { WebSocketProgramUserAccountSubscriber } from './accounts/websocketProgramUserAccountSubscriber'; -export { WebSocketProgramAccountsSubscriberV2 } from './accounts/webSocketProgramAccountsSubscriberV2'; -export { WebSocketDriftClientAccountSubscriberV2 } from './accounts/webSocketDriftClientAccountSubscriberV2'; export * from './accounts/bulkAccountLoader'; export * from './accounts/bulkUserSubscription'; export * from './accounts/bulkUserStatsSubscription'; diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 7d5d78e256..1061bf905b 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -343,6 +343,22 @@ export class User { }; } + public getIsolatePerpPositionTokenAmount(perpMarketIndex: number): BN { + const perpPosition = this.getPerpPosition(perpMarketIndex); + const perpMarket = this.driftClient.getPerpMarketAccount(perpMarketIndex); + const spotMarket = this.driftClient.getSpotMarketAccount( + perpMarket.quoteSpotMarketIndex + ); + if (perpPosition === undefined) { + return ZERO; + } + return getTokenAmount( + perpPosition.isolatedPositionScaledBalance, + spotMarket, + SpotBalanceType.DEPOSIT + ); + } + public getClonedPosition(position: PerpPosition): PerpPosition { const clonedPosition = Object.assign({}, position); return clonedPosition; @@ -580,7 +596,8 @@ export class User { (pos) => !pos.baseAssetAmount.eq(ZERO) || !pos.quoteAssetAmount.eq(ZERO) || - !(pos.openOrders == 0) + !(pos.openOrders == 0) || + pos.isolatedPositionScaledBalance.gt(ZERO) ); } diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index 19e5ca9625..fde3b43d72 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -37,6 +37,7 @@ test_files=( highLeverageMode.ts ifRebalance.ts insuranceFundStake.ts + isolatedPositionDriftClient.ts liquidateBorrowForPerpPnl.ts liquidatePerp.ts liquidatePerpWithFill.ts diff --git a/tests/isolatedPositionDriftClient.ts b/tests/isolatedPositionDriftClient.ts new file mode 100644 index 0000000000..fb91494969 --- /dev/null +++ b/tests/isolatedPositionDriftClient.ts @@ -0,0 +1,556 @@ +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/lpPoolSwap.ts b/tests/lpPoolSwap.ts index 866436c136..30d0c70f30 100644 --- a/tests/lpPoolSwap.ts +++ b/tests/lpPoolSwap.ts @@ -506,13 +506,11 @@ describe('LP Pool', () => { expect(Number(diffOutToken)).to.be.approximately(1001298, 1); console.log( - `in Token: ${inTokenBalanceBefore.amount} -> ${ - inTokenBalanceAfter.amount + `in Token: ${inTokenBalanceBefore.amount} -> ${inTokenBalanceAfter.amount } (${Number(diffInToken) / 1e6})` ); console.log( - `out Token: ${outTokenBalanceBefore.amount} -> ${ - outTokenBalanceAfter.amount + `out Token: ${outTokenBalanceBefore.amount} -> ${outTokenBalanceAfter.amount } (${Number(diffOutToken) / 1e6})` ); }); 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();