diff --git a/CHANGELOG.md b/CHANGELOG.md index fed6cb5d6..d6b072cdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- program: allow settle pnl and spot fills via match when utilization is 100% ([#525](https://github.com/drift-labs/protocol-v2/pull/525)) - program: new update_perp_bid_ask_twap ix ([#548](https://github.com/drift-labs/protocol-v2/pull/548)) - program: dont check price bands for place order ([#556](https://github.com/drift-labs/protocol-v2/pull/556)) diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index 686ec0c48..981afa112 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -635,8 +635,9 @@ pub fn update_pool_balances( // 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, true)? + -get_max_withdraw_for_market_with_token_amount(spot_market, token_amount, false)? .cast::()?; + max_withdraw_amount.max(user_unsettled_pnl) }; diff --git a/programs/drift/src/controller/insurance.rs b/programs/drift/src/controller/insurance.rs index 9632bc505..bf523f5c4 100644 --- a/programs/drift/src/controller/insurance.rs +++ b/programs/drift/src/controller/insurance.rs @@ -555,7 +555,7 @@ pub fn settle_revenue_to_insurance_fund( )?; let depositors_claim = - validate_spot_market_vault_amount(spot_market, spot_market_vault_amount)?.cast::()?; + validate_spot_market_vault_amount(spot_market, spot_market_vault_amount)?; let mut token_amount = get_token_amount( spot_market.revenue_pool.scaled_balance, @@ -563,9 +563,9 @@ pub fn settle_revenue_to_insurance_fund( &SpotBalanceType::Deposit, )?; - if depositors_claim < token_amount { + if depositors_claim < token_amount.cast()? { // only allow half of withdraw available when utilization is high - token_amount = depositors_claim.safe_div(2)?; + token_amount = depositors_claim.max(0).cast::()?.safe_div(2)?; } if spot_market.insurance_fund.user_shares > 0 { diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 656697b7b..37f95dd3c 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -3286,7 +3286,7 @@ pub fn fill_spot_order( let base_market = spot_market_map.get_ref(&market_index)?; let quote_market = spot_market_map.get_quote_spot_market()?; let (max_base_asset_amount, max_quote_asset_amount) = - get_max_fill_amounts(user, order_index, &base_market, "e_market)?; + get_max_fill_amounts(user, order_index, &base_market, "e_market, false)?; max_base_asset_amount == Some(0) || max_quote_asset_amount == Some(0) } else { false @@ -3752,7 +3752,7 @@ pub fn fulfill_spot_order_with_match( } let (taker_max_base_asset_amount, taker_max_quote_asset_amount) = - get_max_fill_amounts(taker, taker_order_index, base_market, quote_market)?; + get_max_fill_amounts(taker, taker_order_index, base_market, quote_market, false)?; let taker_base_asset_amount = if let Some(taker_max_quote_asset_amount) = taker_max_quote_asset_amount { @@ -3772,7 +3772,7 @@ pub fn fulfill_spot_order_with_match( }; let (maker_max_base_asset_amount, maker_max_quote_asset_amount) = - get_max_fill_amounts(maker, maker_order_index, base_market, quote_market)?; + get_max_fill_amounts(maker, maker_order_index, base_market, quote_market, false)?; let maker_base_asset_amount = if let Some(maker_max_quote_asset_amount) = maker_max_quote_asset_amount { @@ -4058,7 +4058,7 @@ pub fn fulfill_spot_order_with_external_market( let taker_order_slot = taker.orders[taker_order_index].slot; let (max_base_asset_amount, max_quote_asset_amount) = - get_max_fill_amounts(taker, taker_order_index, base_market, quote_market)?; + get_max_fill_amounts(taker, taker_order_index, base_market, quote_market, true)?; let taker_base_asset_amount = taker_base_asset_amount.min(max_base_asset_amount.unwrap_or(u64::MAX)); @@ -4206,12 +4206,14 @@ pub fn fulfill_spot_order_with_external_market( "Fill on external spot market lead to unexpected to update direction" )?; + let base_update_direction = + taker.orders[taker_order_index].get_spot_position_update_direction(AssetType::Base); update_spot_balances_and_cumulative_deposits( base_asset_amount_filled.cast()?, - &taker.orders[taker_order_index].get_spot_position_update_direction(AssetType::Base), + &base_update_direction, base_market, taker.force_get_spot_position_mut(base_market.market_index)?, - false, + base_update_direction == SpotBalanceType::Borrow, None, )?; @@ -4222,12 +4224,14 @@ pub fn fulfill_spot_order_with_external_market( "Fill on external market lead to unexpected to update direction" )?; + let quote_update_direction = + taker.orders[taker_order_index].get_spot_position_update_direction(AssetType::Quote); update_spot_balances_and_cumulative_deposits( quote_spot_position_delta.cast()?, - &taker.orders[taker_order_index].get_spot_position_update_direction(AssetType::Quote), + "e_update_direction, quote_market, taker.get_quote_spot_position_mut(), - false, + quote_update_direction == SpotBalanceType::Borrow, Some(quote_asset_amount_filled.cast()?), )?; diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index f54f31a39..b189428c1 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -4680,9 +4680,11 @@ pub mod fulfill_spot_order_with_match { LAMPORTS_PER_SOL_I64, LAMPORTS_PER_SOL_U64, PRICE_PRECISION_I64, PRICE_PRECISION_U64, QUOTE_PRECISION_U64, SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64, }; + use crate::math::spot_balance::calculate_utilization; use crate::state::spot_market::{SpotBalanceType, SpotMarket}; use crate::state::user::{MarketType, Order, OrderType, SpotPosition, User, UserStats}; use crate::test_utils::get_orders; + use crate::SPOT_UTILIZATION_PRECISION; use super::*; @@ -7113,6 +7115,148 @@ pub mod fulfill_spot_order_with_match { assert_eq!(base_filled, 166666666); } + + #[test] + fn max_utilization() { + let mut taker_spot_positions = [SpotPosition::default(); 8]; + taker_spot_positions[0] = SpotPosition { + market_index: 0, + scaled_balance: 101 * SPOT_BALANCE_PRECISION_U64, + balance_type: SpotBalanceType::Deposit, + ..SpotPosition::default() + }; + taker_spot_positions[1] = SpotPosition { + market_index: 1, + open_orders: 1, + open_bids: LAMPORTS_PER_SOL_I64, + ..SpotPosition::default() + }; + let mut taker = User { + orders: get_orders(Order { + market_index: 1, + market_type: MarketType::Spot, + order_type: OrderType::Limit, + direction: PositionDirection::Long, + base_asset_amount: LAMPORTS_PER_SOL_U64, + slot: 0, + price: 100 * PRICE_PRECISION_U64, + ..Order::default() + }), + spot_positions: taker_spot_positions, + ..User::default() + }; + + let mut maker_spot_positions = [SpotPosition::default(); 8]; + maker_spot_positions[1] = SpotPosition { + market_index: 1, + balance_type: SpotBalanceType::Deposit, + scaled_balance: SPOT_BALANCE_PRECISION_U64, + open_orders: 1, + open_asks: -LAMPORTS_PER_SOL_I64, + ..SpotPosition::default() + }; + let mut maker = User { + orders: get_orders(Order { + market_index: 1, + post_only: true, + market_type: MarketType::Spot, + order_type: OrderType::Limit, + direction: PositionDirection::Short, + base_asset_amount: LAMPORTS_PER_SOL_U64, + price: 100 * PRICE_PRECISION_U64, + ..Order::default() + }), + spot_positions: maker_spot_positions, + ..User::default() + }; + + let mut base_market = SpotMarket { + deposit_balance: SPOT_BALANCE_PRECISION, + borrow_balance: SPOT_BALANCE_PRECISION, + utilization_twap: SPOT_UTILIZATION_PRECISION as u64, + ..SpotMarket::default_base_market() + }; + let mut quote_market = SpotMarket { + deposit_balance: 101 * SPOT_BALANCE_PRECISION, + borrow_balance: 101 * SPOT_BALANCE_PRECISION, + utilization_twap: SPOT_UTILIZATION_PRECISION as u64, + ..SpotMarket::default_quote_market() + }; + + let base_utilization = calculate_utilization( + base_market.get_deposits().unwrap(), + base_market.get_borrows().unwrap(), + ) + .unwrap(); + + assert_eq!(base_utilization, SPOT_UTILIZATION_PRECISION); + + let quote_utilization = calculate_utilization( + base_market.get_deposits().unwrap(), + base_market.get_borrows().unwrap(), + ) + .unwrap(); + + assert_eq!(quote_utilization, SPOT_UTILIZATION_PRECISION); + + let now = 1_i64; + let slot = 1_u64; + + let fee_structure = get_fee_structure(); + + let (taker_key, maker_key, filler_key) = get_user_keys(); + + let mut taker_stats = UserStats::default(); + let mut maker_stats = UserStats::default(); + + fulfill_spot_order_with_match( + &mut base_market, + &mut quote_market, + &mut taker, + &mut taker_stats, + 0, + &taker_key, + &mut maker, + &mut Some(&mut maker_stats), + 0, + &maker_key, + None, + None, + &filler_key, + now, + slot, + &mut get_oracle_map(), + &fee_structure, + ) + .unwrap(); + + let taker_quote_position = taker.spot_positions[0]; + assert_eq!(taker_quote_position.scaled_balance, 950000000); + + let taker_base_position = taker.spot_positions[1]; + assert_eq!( + taker_base_position.scaled_balance, + SPOT_BALANCE_PRECISION_U64 + ); + assert_eq!(taker_base_position.open_bids, 0); + assert_eq!(taker_base_position.open_orders, 0); + + let base_utilization = calculate_utilization( + base_market.get_deposits().unwrap(), + base_market.get_borrows().unwrap(), + ) + .unwrap(); + + assert_eq!(base_utilization, SPOT_UTILIZATION_PRECISION); + + let quote_utilization = calculate_utilization( + base_market.get_deposits().unwrap(), + base_market.get_borrows().unwrap(), + ) + .unwrap(); + + assert_eq!(quote_utilization, SPOT_UTILIZATION_PRECISION); + } } pub mod fulfill_spot_order { diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index bfc764f2c..fed6f85f8 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -62,7 +62,6 @@ pub fn settle_pnl( crate::controller::lp::settle_funding_payment_then_lp(user, user_key, &mut market, now)?; let oracle_price = oracle_map.get_price_data(&market.amm.oracle)?.price; - drop(market); let position_index = get_position_index(&user.perp_positions, market_index)?; @@ -129,7 +128,6 @@ pub fn settle_pnl( user_unsettled_pnl, now, )?; - if user_unsettled_pnl == 0 { msg!("User has no unsettled pnl for market {}", market_index); return Ok(()); diff --git a/programs/drift/src/controller/spot_balance.rs b/programs/drift/src/controller/spot_balance.rs index 9f7f60059..d8e5327bd 100644 --- a/programs/drift/src/controller/spot_balance.rs +++ b/programs/drift/src/controller/spot_balance.rs @@ -204,7 +204,7 @@ pub fn update_spot_balances( update_direction: &SpotBalanceType, spot_market: &mut SpotMarket, spot_balance: &mut dyn SpotBalance, - force_round_up: bool, + is_leaving_drift: bool, ) -> DriftResult { let increase_user_existing_balance = update_direction == spot_balance.balance_type(); if increase_user_existing_balance { @@ -225,7 +225,7 @@ pub fn update_spot_balances( // determine how much to reduce balance based on size of current token amount let (token_delta, balance_delta) = if current_token_amount > token_amount { let round_up = - force_round_up || spot_balance.balance_type() == &SpotBalanceType::Borrow; + is_leaving_drift || spot_balance.balance_type() == &SpotBalanceType::Borrow; let balance_delta = get_spot_balance( token_amount, spot_market, @@ -252,7 +252,7 @@ pub fn update_spot_balances( } } - if let SpotBalanceType::Borrow = update_direction { + if is_leaving_drift && update_direction == &SpotBalanceType::Borrow { let deposit_token_amount = get_token_amount( spot_market.deposit_balance, spot_market, @@ -293,13 +293,15 @@ pub fn transfer_spot_balances( return Ok(()); } - validate!( - spot_market.deposit_balance >= from_spot_balance.balance(), - ErrorCode::InvalidSpotMarketState, - "spot_market.deposit_balance={} lower than individual spot balance={}", - spot_market.deposit_balance, - from_spot_balance.balance() - )?; + if from_spot_balance.balance_type() == &SpotBalanceType::Deposit { + validate!( + spot_market.deposit_balance >= from_spot_balance.balance(), + ErrorCode::InvalidSpotMarketState, + "spot_market.deposit_balance={} lower than individual spot balance={}", + spot_market.deposit_balance, + from_spot_balance.balance() + )?; + } update_spot_balances( token_amount.unsigned_abs(), diff --git a/programs/drift/src/controller/spot_position.rs b/programs/drift/src/controller/spot_position.rs index 4a161eead..ac8abfbfa 100644 --- a/programs/drift/src/controller/spot_position.rs +++ b/programs/drift/src/controller/spot_position.rs @@ -66,7 +66,7 @@ pub fn update_spot_balances_and_cumulative_deposits( update_direction: &SpotBalanceType, spot_market: &mut SpotMarket, spot_position: &mut SpotPosition, - force_round_up: bool, + is_leaving_drift: bool, cumulative_deposit_delta: Option, ) -> DriftResult { update_spot_balances( @@ -74,7 +74,7 @@ pub fn update_spot_balances_and_cumulative_deposits( update_direction, spot_market, spot_position, - force_round_up, + is_leaving_drift, )?; let cumulative_deposit_delta = cumulative_deposit_delta.unwrap_or(token_amount); diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index c0cf17325..d2ea881eb 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -746,16 +746,19 @@ pub fn get_max_fill_amounts( user_order_index: usize, base_market: &SpotMarket, quote_market: &SpotMarket, + is_leaving_drift: bool, ) -> DriftResult<(Option, Option)> { let direction: PositionDirection = user.orders[user_order_index].direction; match direction { PositionDirection::Long => { - let max_quote = get_max_fill_amounts_for_market(user, quote_market)?.cast::()?; + let max_quote = get_max_fill_amounts_for_market(user, quote_market, is_leaving_drift)? + .cast::()?; Ok((None, Some(max_quote))) } PositionDirection::Short => { let max_base = standardize_base_asset_amount( - get_max_fill_amounts_for_market(user, base_market)?.cast::()?, + get_max_fill_amounts_for_market(user, base_market, is_leaving_drift)? + .cast::()?, base_market.order_step_size, )?; Ok((Some(max_base), None)) @@ -763,10 +766,14 @@ pub fn get_max_fill_amounts( } } -fn get_max_fill_amounts_for_market(user: &User, market: &SpotMarket) -> DriftResult { +fn get_max_fill_amounts_for_market( + user: &User, + market: &SpotMarket, + is_leaving_drift: bool, +) -> DriftResult { let position_index = user.get_spot_position_index(market.market_index)?; let token_amount = user.spot_positions[position_index].get_signed_token_amount(market)?; - get_max_withdraw_for_market_with_token_amount(market, token_amount, false) + get_max_withdraw_for_market_with_token_amount(market, token_amount, is_leaving_drift) } pub fn find_fallback_maker_order( diff --git a/programs/drift/src/math/orders/tests.rs b/programs/drift/src/math/orders/tests.rs index 48039e09f..18d3e928d 100644 --- a/programs/drift/src/math/orders/tests.rs +++ b/programs/drift/src/math/orders/tests.rs @@ -571,7 +571,7 @@ mod get_max_fill_amounts { }; let (max_base, max_quote) = - get_max_fill_amounts(&user, 0, &base_market, "e_market).unwrap(); + get_max_fill_amounts(&user, 0, &base_market, "e_market, true).unwrap(); assert_eq!(max_base, Some(100 * LAMPORTS_PER_SOL)); assert_eq!(max_quote, None); @@ -609,7 +609,7 @@ mod get_max_fill_amounts { }; let (max_base, max_quote) = - get_max_fill_amounts(&user, 0, &base_market, "e_market).unwrap(); + get_max_fill_amounts(&user, 0, &base_market, "e_market, true).unwrap(); assert_eq!(max_base, Some(0)); assert_eq!(max_quote, None); @@ -650,7 +650,7 @@ mod get_max_fill_amounts { }; let (max_base, max_quote) = - get_max_fill_amounts(&user, 0, &base_market, "e_market).unwrap(); + get_max_fill_amounts(&user, 0, &base_market, "e_market, true).unwrap(); assert_eq!(max_base, Some(16666666666)); assert_eq!(max_quote, None); @@ -692,7 +692,7 @@ mod get_max_fill_amounts { }; let (max_base, max_quote) = - get_max_fill_amounts(&user, 0, &base_market, "e_market).unwrap(); + get_max_fill_amounts(&user, 0, &base_market, "e_market, true).unwrap(); assert_eq!(max_base, None); assert_eq!(max_quote, Some(100 * QUOTE_PRECISION_U64)); @@ -730,7 +730,7 @@ mod get_max_fill_amounts { }; let (max_base, max_quote) = - get_max_fill_amounts(&user, 0, &base_market, "e_market).unwrap(); + get_max_fill_amounts(&user, 0, &base_market, "e_market, true).unwrap(); assert_eq!(max_base, None); assert_eq!(max_quote, Some(0)); @@ -772,7 +772,7 @@ mod get_max_fill_amounts { }; let (max_base, max_quote) = - get_max_fill_amounts(&user, 0, &base_market, "e_market).unwrap(); + get_max_fill_amounts(&user, 0, &base_market, "e_market, true).unwrap(); assert_eq!(max_base, None); assert_eq!(max_quote, Some(16666666)); diff --git a/programs/drift/src/math/spot_withdraw.rs b/programs/drift/src/math/spot_withdraw.rs index 843c71411..766097ccc 100644 --- a/programs/drift/src/math/spot_withdraw.rs +++ b/programs/drift/src/math/spot_withdraw.rs @@ -103,9 +103,7 @@ pub fn calculate_token_utilization_limits( let max_withdraw_utilization: u128 = spot_market.optimal_utilization.cast::()?.max( spot_market.utilization_twap.cast::()?.safe_add( - SPOT_UTILIZATION_PRECISION - .safe_sub(spot_market.utilization_twap.cast()?)? - .safe_div(2)?, + SPOT_UTILIZATION_PRECISION.saturating_sub(spot_market.utilization_twap.cast()?) / 2, )?, ); @@ -202,7 +200,7 @@ pub fn check_withdraw_limits( pub fn get_max_withdraw_for_market_with_token_amount( spot_market: &SpotMarket, token_amount: i128, - is_pool_transfer: bool, + is_leaving_drift: bool, ) -> DriftResult { let deposit_token_amount = get_token_amount( spot_market.deposit_balance, @@ -216,8 +214,13 @@ pub fn get_max_withdraw_for_market_with_token_amount( &SpotBalanceType::Borrow, )?; - let (min_deposit_token_for_utilization, max_borrow_token_for_utilization) = - calculate_token_utilization_limits(deposit_token_amount, borrow_token_amount, spot_market)?; + // if leaving drift, need to consider utilization limits + let (min_deposit_token_for_utilization, max_borrow_token_for_utilization) = if is_leaving_drift + { + calculate_token_utilization_limits(deposit_token_amount, borrow_token_amount, spot_market)? + } else { + (0, u128::MAX) + }; let mut max_withdraw_amount = 0_u128; if token_amount > 0 { @@ -229,7 +232,7 @@ pub fn get_max_withdraw_for_market_with_token_amount( let withdraw_limit = deposit_token_amount.saturating_sub(min_deposit_token); let token_amount = token_amount.unsigned_abs(); - if withdraw_limit <= token_amount && !is_pool_transfer { + if withdraw_limit <= token_amount && is_leaving_drift { return Ok(withdraw_limit); } @@ -252,7 +255,7 @@ pub fn get_max_withdraw_for_market_with_token_amount( max_withdraw_amount.safe_add(borrow_limit) } -pub fn validate_spot_balances(spot_market: &SpotMarket) -> DriftResult { +pub fn validate_spot_balances(spot_market: &SpotMarket) -> DriftResult { let depositors_amount: u64 = get_token_amount( spot_market.deposit_balance, spot_market, @@ -266,14 +269,6 @@ pub fn validate_spot_balances(spot_market: &SpotMarket) -> DriftResult { )? .cast()?; - validate!( - depositors_amount >= borrowers_amount, - ErrorCode::SpotMarketBalanceInvariantViolated, - "depositors_amount={} less than borrowers_amount={}", - depositors_amount, - borrowers_amount - )?; - let revenue_amount: u64 = get_token_amount( spot_market.revenue_pool.scaled_balance, spot_market, @@ -281,7 +276,9 @@ pub fn validate_spot_balances(spot_market: &SpotMarket) -> DriftResult { )? .cast()?; - let depositors_claim = depositors_amount - borrowers_amount; + let depositors_claim = depositors_amount + .cast::()? + .safe_sub(borrowers_amount.cast()?)?; validate!( revenue_amount <= depositors_amount, @@ -299,11 +296,11 @@ pub fn validate_spot_balances(spot_market: &SpotMarket) -> DriftResult { pub fn validate_spot_market_vault_amount( spot_market: &SpotMarket, vault_amount: u64, -) -> DriftResult { +) -> DriftResult { let depositors_claim = validate_spot_balances(spot_market)?; validate!( - vault_amount >= depositors_claim, + vault_amount.cast::()? >= depositors_claim, ErrorCode::SpotMarketVaultInvariantViolated, "spot market vault ={} holds less than remaining depositor claims = {}", vault_amount, diff --git a/programs/drift/src/state/spot_market.rs b/programs/drift/src/state/spot_market.rs index 931958284..6a35b6840 100644 --- a/programs/drift/src/state/spot_market.rs +++ b/programs/drift/src/state/spot_market.rs @@ -354,6 +354,10 @@ impl SpotMarket { get_token_amount(self.deposit_balance, self, &SpotBalanceType::Deposit) } + pub fn get_borrows(&self) -> DriftResult { + get_token_amount(self.borrow_balance, self, &SpotBalanceType::Borrow) + } + pub fn validate_max_token_deposits(&self) -> DriftResult { let deposits = self.get_deposits()?; let max_token_deposits = self.max_token_deposits.cast::()?; diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index fbd41a92c..128dee623 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -724,7 +724,6 @@ impl PerpPosition { pub fn get_claimable_pnl(&self, oracle_price: i64, pnl_pool_excess: i128) -> DriftResult { let (_, unrealized_pnl) = calculate_base_asset_value_and_pnl_with_oracle_price(self, oracle_price)?; - if unrealized_pnl > 0 { // this limits the amount of positive pnl that can be settled to be the amount of positive pnl // realized by reducing/closing position diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index 92c8e0184..08e90b347 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -29,6 +29,7 @@ test_files=( liquidateMaxLps.ts order.ts spotDepositWithdraw.ts + spotWithdrawUtil100.ts prepegMarketOrderBaseAssetAmount.ts updateAMM.ts repegAndSpread.ts diff --git a/tests/phoenixTest.ts b/tests/phoenixTest.ts index 5ac27a2f7..7f53328ee 100644 --- a/tests/phoenixTest.ts +++ b/tests/phoenixTest.ts @@ -436,7 +436,7 @@ describe('phoenix spot market', () => { takerQuoteSpotBalance.balanceType ); console.log(quoteTokenAmount.toString()); - assert(quoteTokenAmount.eq(new BN(99900000))); + assert(quoteTokenAmount.eq(new BN(99899999))); const baseTokenAmount = getTokenAmount( takerBaseSpotBalance.scaledBalance, @@ -595,7 +595,7 @@ describe('phoenix spot market', () => { takerQuoteSpotBalance.balanceType ); console.log(quoteTokenAmount.toString()); - assert(quoteTokenAmount.eq(new BN(199800000))); + assert(quoteTokenAmount.eq(new BN(199799999))); const baseTokenAmount = getTokenAmount( takerBaseSpotBalance.scaledBalance, diff --git a/tests/serumTest.ts b/tests/serumTest.ts index b713edcb8..0069e0261 100644 --- a/tests/serumTest.ts +++ b/tests/serumTest.ts @@ -289,7 +289,7 @@ describe('serum spot market', () => { takerQuoteSpotBalance.balanceType ); console.log(quoteTokenAmount.toString()); - assert(quoteTokenAmount.eq(new BN(99900000))); + assert(quoteTokenAmount.eq(new BN(99899999))); const baseTokenAmount = getTokenAmount( takerBaseSpotBalance.scaledBalance, @@ -397,7 +397,7 @@ describe('serum spot market', () => { takerQuoteSpotBalance.balanceType ); console.log(quoteTokenAmount.toString()); - assert(quoteTokenAmount.eq(new BN(199800000))); + assert(quoteTokenAmount.eq(new BN(199799999))); const baseTokenAmount = getTokenAmount( takerBaseSpotBalance.scaledBalance, @@ -508,7 +508,7 @@ describe('serum spot market', () => { takerQuoteSpotBalance.balanceType ); console.log(quoteTokenAmount.toString()); - assert(quoteTokenAmount.eq(new BN(99700000))); // paid ~$.30 + assert(quoteTokenAmount.eq(new BN(99699999))); // paid ~$.30 const baseTokenAmount = getTokenAmount( takerBaseSpotBalance.scaledBalance, @@ -602,7 +602,7 @@ describe('serum spot market', () => { takerQuoteSpotBalance.balanceType ); console.log(quoteTokenAmount.toString()); - assert(quoteTokenAmount.eq(new BN(199600000))); // paid ~$.40 + assert(quoteTokenAmount.eq(new BN(199599999))); // paid ~$.40 const baseTokenAmount = getTokenAmount( takerBaseSpotBalance.scaledBalance, diff --git a/tests/spotWithdrawUtil100.ts b/tests/spotWithdrawUtil100.ts new file mode 100644 index 000000000..4fd579d6c --- /dev/null +++ b/tests/spotWithdrawUtil100.ts @@ -0,0 +1,799 @@ +import * as anchor from '@coral-xyz/anchor'; +import { assert } from 'chai'; + +import { Program } from '@coral-xyz/anchor'; +import { setFeedPrice } from './testHelpers'; +import { PublicKey } from '@solana/web3.js'; +import { + PositionDirection, + User, + BASE_PRECISION, + getLimitOrderParams, + PostOnlyParams, + MarketStatus, +} from '../sdk/src'; + +import { + TestClient, + BN, + EventSubscriber, + SPOT_MARKET_RATE_PRECISION, + SpotBalanceType, + isVariant, + OracleSource, + SPOT_MARKET_WEIGHT_PRECISION, + SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION, + OracleInfo, + QUOTE_PRECISION, + ZERO, + ONE, + SPOT_MARKET_BALANCE_PRECISION, + PRICE_PRECISION, + BulkAccountLoader, +} from '../sdk/src'; + +import { + createUserWithUSDCAccount, + createUserWithUSDCAndWSOLAccount, + mockOracle, + mockUSDCMint, + mockUserUSDCAccount, + printTxLogs, + sleep, +} from './testHelpers'; +import { + getBalance, + calculateInterestAccumulated, + calculateUtilization, +} from '../sdk/src/math/spotBalance'; +import { NATIVE_MINT } from '@solana/spl-token'; + +describe('test function when spot market at >= 100% util', () => { + const provider = anchor.AnchorProvider.local(undefined, { + preflightCommitment: 'confirmed', + skipPreflight: false, + commitment: 'confirmed', + }); + const connection = provider.connection; + anchor.setProvider(provider); + const chProgram = anchor.workspace.Drift as Program; + + let admin: TestClient; + const eventSubscriber = new EventSubscriber(connection, chProgram, { + commitment: 'recent', + }); + eventSubscriber.subscribe(); + + const bulkAccountLoader = new BulkAccountLoader(connection, 'confirmed', 1); + + let solOracle: PublicKey; + + let usdcMint; + + let firstUserDriftClient: TestClient; + let firstUserDriftClientUSDCAccount: PublicKey; + + let secondUserDriftClient: TestClient; + let secondUserDriftClientWSOLAccount: PublicKey; + let secondUserDriftClientUSDCAccount: PublicKey; + + const usdcAmount = new BN(10 * 10 ** 6); + const largeUsdcAmount = new BN(10_000 * 10 ** 6); + + const solAmount = new BN(1 * 10 ** 9); + + let marketIndexes: number[]; + let spotMarketIndexes: number[]; + let oracleInfos: OracleInfo[]; + + before(async () => { + usdcMint = await mockUSDCMint(provider); + await mockUserUSDCAccount(usdcMint, largeUsdcAmount, provider); + + solOracle = await mockOracle(30); + + marketIndexes = [0]; + spotMarketIndexes = [0, 1]; + oracleInfos = [{ publicKey: solOracle, source: OracleSource.PYTH }]; + + admin = new TestClient({ + connection, + wallet: provider.wallet, + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + perpMarketIndexes: marketIndexes, + spotMarketIndexes: spotMarketIndexes, + oracleInfos, + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + + await admin.initialize(usdcMint.publicKey, true); + await admin.subscribe(); + }); + + after(async () => { + await admin.unsubscribe(); + await eventSubscriber.unsubscribe(); + await firstUserDriftClient.unsubscribe(); + await secondUserDriftClient.unsubscribe(); + // await thirdUserDriftClient.unsubscribe(); + }); + + it('Initialize USDC Market', async () => { + const optimalUtilization = SPOT_MARKET_RATE_PRECISION.div( + new BN(2) + ).toNumber(); // 50% utilization + const optimalRate = SPOT_MARKET_RATE_PRECISION.mul(new BN(20)).toNumber(); // 2000% APR + const maxRate = SPOT_MARKET_RATE_PRECISION.mul(new BN(500)).toNumber(); // 50000% APR + const initialAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const initialLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + await admin.initializeSpotMarket( + usdcMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + PublicKey.default, + OracleSource.QUOTE_ASSET, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight + ); + const txSig = await admin.updateWithdrawGuardThreshold( + 0, + new BN(10 ** 10).mul(QUOTE_PRECISION) + ); + await printTxLogs(connection, txSig); + await admin.fetchAccounts(); + const spotMarket = await admin.getSpotMarketAccount(0); + assert(spotMarket.marketIndex === 0); + assert(spotMarket.optimalUtilization === optimalUtilization); + assert(spotMarket.optimalBorrowRate === optimalRate); + assert(spotMarket.maxBorrowRate === maxRate); + assert(spotMarket.decimals === 6); + assert( + spotMarket.cumulativeBorrowInterest.eq( + SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION + ) + ); + assert( + spotMarket.cumulativeDepositInterest.eq( + SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION + ) + ); + assert(spotMarket.initialAssetWeight === initialAssetWeight); + assert(spotMarket.maintenanceAssetWeight === maintenanceAssetWeight); + assert(spotMarket.initialLiabilityWeight === initialLiabilityWeight); + assert(spotMarket.maintenanceAssetWeight === maintenanceAssetWeight); + + assert(admin.getStateAccount().numberOfSpotMarkets === 1); + }); + + it('Initialize SOL spot/perp Market', async () => { + const optimalUtilization = SPOT_MARKET_RATE_PRECISION.div( + new BN(2) + ).toNumber(); // 50% utilization + const optimalRate = SPOT_MARKET_RATE_PRECISION.mul(new BN(20)).toNumber(); // 2000% APR + const maxRate = SPOT_MARKET_RATE_PRECISION.mul(new BN(500)).toNumber(); // 50000% APR + const initialAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.mul(new BN(8)) + .div(new BN(10)) + .toNumber(); + const maintenanceAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.mul(new BN(9)) + .div(new BN(10)) + .toNumber(); + const initialLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.mul(new BN(12)) + .div(new BN(10)) + .toNumber(); + const maintenanceLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.mul( + new BN(11) + ) + .div(new BN(10)) + .toNumber(); + + await admin.initializeSpotMarket( + NATIVE_MINT, + optimalUtilization, + optimalRate, + maxRate, + solOracle, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight + ); + + const mantissaSqrtScale = new BN(100000); + const ammInitialQuoteAssetReserve = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetReserve = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + await admin.initializePerpMarket( + 0, + solOracle, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + new BN(1), + new BN(30_000_000), + undefined, + 1000, + 900 // easy to liq + ); + await admin.updatePerpMarketStatus(0, MarketStatus.ACTIVE); + await admin.updatePerpMarketBaseSpread(0, 2000); + await admin.updatePerpMarketCurveUpdateIntensity(0, 100); + + const txSig = await admin.updateWithdrawGuardThreshold( + 1, + new BN(10 ** 10).mul(QUOTE_PRECISION) + ); + await printTxLogs(connection, txSig); + await admin.fetchAccounts(); + const spotMarket = await admin.getSpotMarketAccount(1); + assert(spotMarket.marketIndex === 1); + assert(spotMarket.optimalUtilization === optimalUtilization); + assert(spotMarket.optimalBorrowRate === optimalRate); + assert(spotMarket.maxBorrowRate === maxRate); + assert(spotMarket.decimals === 9); + assert( + spotMarket.cumulativeBorrowInterest.eq( + SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION + ) + ); + assert( + spotMarket.cumulativeDepositInterest.eq( + SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION + ) + ); + assert(spotMarket.initialAssetWeight === initialAssetWeight); + assert(spotMarket.maintenanceAssetWeight === maintenanceAssetWeight); + assert(spotMarket.initialLiabilityWeight === initialLiabilityWeight); + assert(spotMarket.maintenanceAssetWeight === maintenanceAssetWeight); + + console.log(spotMarket.historicalOracleData); + assert(spotMarket.historicalOracleData.lastOraclePriceTwapTs.eq(ZERO)); + + assert( + spotMarket.historicalOracleData.lastOraclePrice.eq( + new BN(30 * PRICE_PRECISION.toNumber()) + ) + ); + assert( + spotMarket.historicalOracleData.lastOraclePriceTwap.eq( + new BN(30 * PRICE_PRECISION.toNumber()) + ) + ); + assert( + spotMarket.historicalOracleData.lastOraclePriceTwap5Min.eq( + new BN(30 * PRICE_PRECISION.toNumber()) + ) + ); + + assert(admin.getStateAccount().numberOfSpotMarkets === 2); + }); + + it('First User Deposit USDC', async () => { + [firstUserDriftClient, firstUserDriftClientUSDCAccount] = + await createUserWithUSDCAccount( + provider, + usdcMint, + chProgram, + largeUsdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader + ); + + const marketIndex = 0; + await sleep(100); + await firstUserDriftClient.fetchAccounts(); + const txSig = await firstUserDriftClient.deposit( + usdcAmount, + marketIndex, + firstUserDriftClientUSDCAccount + ); + await printTxLogs(connection, txSig); + + const spotMarket = await admin.getSpotMarketAccount(marketIndex); + assert( + spotMarket.depositBalance.eq( + new BN(10 * SPOT_MARKET_BALANCE_PRECISION.toNumber()) + ) + ); + + const vaultAmount = new BN( + ( + await provider.connection.getTokenAccountBalance(spotMarket.vault) + ).value.amount + ); + assert(vaultAmount.eq(usdcAmount)); + + const expectedBalance = getBalance( + usdcAmount, + spotMarket, + SpotBalanceType.DEPOSIT + ); + const spotPosition = firstUserDriftClient.getUserAccount().spotPositions[0]; + assert(isVariant(spotPosition.balanceType, 'deposit')); + assert(spotPosition.scaledBalance.eq(expectedBalance)); + + assert(firstUserDriftClient.getUserAccount().totalDeposits.eq(usdcAmount)); + }); + + it('Second User Deposit SOL', async () => { + [ + secondUserDriftClient, + secondUserDriftClientWSOLAccount, + secondUserDriftClientUSDCAccount, + ] = await createUserWithUSDCAndWSOLAccount( + provider, + usdcMint, + chProgram, + solAmount.mul(new BN(1000)), + largeUsdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader + ); + + const marketIndex = 1; + const txSig = await secondUserDriftClient.deposit( + solAmount, + marketIndex, + secondUserDriftClientWSOLAccount + ); + await printTxLogs(connection, txSig); + + const spotMarket = await admin.getSpotMarketAccount(marketIndex); + assert(spotMarket.depositBalance.eq(SPOT_MARKET_BALANCE_PRECISION)); + console.log(spotMarket.historicalOracleData); + assert(spotMarket.historicalOracleData.lastOraclePriceTwapTs.gt(ZERO)); + assert( + spotMarket.historicalOracleData.lastOraclePrice.eq( + new BN(30 * PRICE_PRECISION.toNumber()) + ) + ); + assert( + spotMarket.historicalOracleData.lastOraclePriceTwap.eq( + new BN(30 * PRICE_PRECISION.toNumber()) + ) + ); + assert( + spotMarket.historicalOracleData.lastOraclePriceTwap5Min.eq( + new BN(30 * PRICE_PRECISION.toNumber()) + ) + ); + + const vaultAmount = new BN( + ( + await provider.connection.getTokenAccountBalance(spotMarket.vault) + ).value.amount + ); + assert(vaultAmount.eq(solAmount)); + + const expectedBalance = getBalance( + solAmount, + spotMarket, + SpotBalanceType.DEPOSIT + ); + const spotPosition = + secondUserDriftClient.getUserAccount().spotPositions[1]; + assert(isVariant(spotPosition.balanceType, 'deposit')); + assert(spotPosition.scaledBalance.eq(expectedBalance)); + + assert( + secondUserDriftClient + .getUserAccount() + .totalDeposits.eq(new BN(30).mul(PRICE_PRECISION)) + ); + }); + + it('Second User Withdraw all USDC', async () => { + const marketIndex = 0; + const withdrawAmount = usdcAmount.sub(ONE); // cause borrow rounding + const txSig = await secondUserDriftClient.withdraw( + withdrawAmount, + marketIndex, + secondUserDriftClientUSDCAccount + ); + await printTxLogs(connection, txSig); + + const spotMarket = await admin.getSpotMarketAccount(marketIndex); + const expectedBorrowBalance = new BN(9999999001); + console.log('borrowBalance:', spotMarket.borrowBalance.toString()); + assert(spotMarket.borrowBalance.eq(expectedBorrowBalance)); + + const vaultAmount = new BN( + ( + await provider.connection.getTokenAccountBalance(spotMarket.vault) + ).value.amount + ); + const expectedVaultAmount = usdcAmount.sub(withdrawAmount); + assert(vaultAmount.eq(expectedVaultAmount)); + + const expectedBalance = getBalance( + withdrawAmount, + spotMarket, + SpotBalanceType.BORROW + ); + + const spotPosition = + secondUserDriftClient.getUserAccount().spotPositions[0]; + assert(isVariant(spotPosition.balanceType, 'borrow')); + assert(spotPosition.scaledBalance.eq(expectedBalance)); + + assert( + secondUserDriftClient.getUserAccount().totalWithdraws.eq(withdrawAmount) + ); + }); + + it('Update Cumulative Interest with 100% utilization', async () => { + const usdcmarketIndex = 0; + const oldSpotMarketAccount = + firstUserDriftClient.getSpotMarketAccount(usdcmarketIndex); + + await sleep(200); + + const txSig = await firstUserDriftClient.updateSpotMarketCumulativeInterest( + usdcmarketIndex + ); + await printTxLogs(connection, txSig); + + await firstUserDriftClient.fetchAccounts(); + const newSpotMarketAccount = + firstUserDriftClient.getSpotMarketAccount(usdcmarketIndex); + + const expectedInterestAccumulated = calculateInterestAccumulated( + oldSpotMarketAccount, + newSpotMarketAccount.lastInterestTs + ); + const expectedCumulativeDepositInterest = + oldSpotMarketAccount.cumulativeDepositInterest.add( + expectedInterestAccumulated.depositInterest + ); + const expectedCumulativeBorrowInterest = + oldSpotMarketAccount.cumulativeBorrowInterest.add( + expectedInterestAccumulated.borrowInterest + ); + + assert( + newSpotMarketAccount.cumulativeDepositInterest.eq( + expectedCumulativeDepositInterest + ) + ); + console.log( + newSpotMarketAccount.cumulativeBorrowInterest.sub(ONE).toString(), + expectedCumulativeBorrowInterest.toString() + ); + + // inconcistent time leads to slight differences over runs? + assert( + newSpotMarketAccount.cumulativeBorrowInterest + .sub(ONE) + .eq(expectedCumulativeBorrowInterest) || + newSpotMarketAccount.cumulativeBorrowInterest.eq( + expectedCumulativeBorrowInterest + ) + ); + }); + + it('Update Cumulative Interest with 100% utilization (again)', async () => { + const usdcmarketIndex = 0; + const oldSpotMarketAccount = + firstUserDriftClient.getSpotMarketAccount(usdcmarketIndex); + + await sleep(10000); + + const txSig = await firstUserDriftClient.updateSpotMarketCumulativeInterest( + usdcmarketIndex + ); + await printTxLogs(connection, txSig); + + await firstUserDriftClient.fetchAccounts(); + const newSpotMarketAccount = + firstUserDriftClient.getSpotMarketAccount(usdcmarketIndex); + + const expectedInterestAccumulated = calculateInterestAccumulated( + oldSpotMarketAccount, + newSpotMarketAccount.lastInterestTs + ); + const expectedCumulativeDepositInterest = + oldSpotMarketAccount.cumulativeDepositInterest.add( + expectedInterestAccumulated.depositInterest + ); + const expectedCumulativeBorrowInterest = + oldSpotMarketAccount.cumulativeBorrowInterest.add( + expectedInterestAccumulated.borrowInterest + ); + + assert( + newSpotMarketAccount.cumulativeDepositInterest.eq( + expectedCumulativeDepositInterest + ) + ); + console.log( + newSpotMarketAccount.cumulativeBorrowInterest.sub(ONE).toString(), + expectedCumulativeBorrowInterest.toString() + ); + + // inconcistent time leads to slight differences over runs? + assert( + newSpotMarketAccount.cumulativeBorrowInterest + .sub(ONE) + .eq(expectedCumulativeBorrowInterest) || + newSpotMarketAccount.cumulativeBorrowInterest.eq( + expectedCumulativeBorrowInterest + ) + ); + }); + + it('trade spot at 100% util', async () => { + const spotMarketAccountAfter = + secondUserDriftClient.getSpotMarketAccount(0); + const util12 = calculateUtilization(spotMarketAccountAfter, ZERO); + console.log('USDC utilization:', util12.toNumber() / 1e4, '%'); + + const marketIndex = 1; + + await firstUserDriftClient.updateUserMarginTradingEnabled(true, 0); + + const takerDriftClientUser = new User({ + driftClient: firstUserDriftClient, + userAccountPublicKey: + await firstUserDriftClient.getUserAccountPublicKey(), + }); + await takerDriftClientUser.subscribe(); + + const takerUSDCBefore = takerDriftClientUser.getTokenAmount(0); + const takerSOLBefore = takerDriftClientUser.getTokenAmount(1); + + const makerUSDCBefore = secondUserDriftClient.getUser().getTokenAmount(0); + const makerSOLBefore = secondUserDriftClient.getUser().getTokenAmount(1); + + const baseAssetAmount = BASE_PRECISION; + const takerOrderParams = getLimitOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount, + price: new BN(31).mul(PRICE_PRECISION), + auctionStartPrice: new BN(30).mul(PRICE_PRECISION), + auctionEndPrice: new BN(31).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId: 1, + postOnly: PostOnlyParams.NONE, + }); + await firstUserDriftClient.placeSpotOrder(takerOrderParams); + await takerDriftClientUser.fetchAccounts(); + const order = takerDriftClientUser.getOrderByUserOrderId(1); + assert(!order.postOnly); + + const makerOrderParams = getLimitOrderParams({ + marketIndex, + direction: PositionDirection.SHORT, + baseAssetAmount, + price: new BN(30).mul(PRICE_PRECISION), + userOrderId: 1, + postOnly: PostOnlyParams.MUST_POST_ONLY, + immediateOrCancel: true, + }); + + const txSig2 = await secondUserDriftClient.placeAndMakeSpotOrder( + makerOrderParams, + { + taker: await firstUserDriftClient.getUserAccountPublicKey(), + order: firstUserDriftClient.getOrderByUserId(1), + takerUserAccount: firstUserDriftClient.getUserAccount(), + takerStats: firstUserDriftClient.getUserStatsAccountPublicKey(), + } + ); + + await printTxLogs(connection, txSig2); + await firstUserDriftClient.fetchAccounts(); + await takerDriftClientUser.fetchAccounts(); + await secondUserDriftClient.fetchAccounts(); + + const takerUSDCAfter = takerDriftClientUser.getTokenAmount(0); + const takerSOLAfter = takerDriftClientUser.getTokenAmount(1); + + const makerUSDCAfter = secondUserDriftClient.getUser().getTokenAmount(0); + const makerSOLAfter = secondUserDriftClient.getUser().getTokenAmount(1); + + console.log( + 'taker usdc:', + takerUSDCBefore.toString(), + '->', + takerUSDCAfter.toString() + ); + console.log( + 'taker sol:', + takerSOLBefore.toString(), + '->', + takerSOLAfter.toString() + ); + + console.log( + 'maker usdc:', + makerUSDCBefore.toString(), + '->', + makerUSDCAfter.toString() + ); + console.log( + 'maker sol:', + makerSOLBefore.toString(), + '->', + makerSOLAfter.toString() + ); + + assert(makerUSDCBefore.lt(ZERO)); + assert(makerUSDCAfter.gt(ZERO)); + assert(takerSOLBefore.eq(ZERO)); + assert(takerSOLAfter.gt(ZERO)); + + await takerDriftClientUser.unsubscribe(); + }); + + it('trade/settle perp pnl at 100% util', async () => { + const spotMarketAccountAfter = + secondUserDriftClient.getSpotMarketAccount(0); + const util12 = calculateUtilization(spotMarketAccountAfter, ZERO); + console.log('USDC utilization:', util12.toNumber() / 1e4, '%'); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + const takerOrderParams = getLimitOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount, + price: new BN(34).mul(PRICE_PRECISION), + auctionStartPrice: new BN(30.01 * PRICE_PRECISION.toNumber()), + auctionEndPrice: new BN(32).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId: 1, + postOnly: PostOnlyParams.NONE, + }); + + const takerDriftClientUser = new User({ + driftClient: firstUserDriftClient, + userAccountPublicKey: + await firstUserDriftClient.getUserAccountPublicKey(), + }); + await takerDriftClientUser.subscribe(); + + const firstUserSpot = await takerDriftClientUser.getSpotPosition(0); + console.log('takerDriftClientUser spot 0:', firstUserSpot); + console.log( + 'taker token amount:', + takerDriftClientUser.getTokenAmount(0).toString() + ); + assert(isVariant(firstUserSpot.balanceType, 'borrow')); + + await firstUserDriftClient.placePerpOrder(takerOrderParams); + await takerDriftClientUser.fetchAccounts(); + const order = takerDriftClientUser.getOrderByUserOrderId(1); + assert(!order.postOnly); + + const makerOrderParams = getLimitOrderParams({ + marketIndex, + direction: PositionDirection.SHORT, + baseAssetAmount, + price: new BN(30.001 * PRICE_PRECISION.toNumber()), + userOrderId: 1, + postOnly: PostOnlyParams.MUST_POST_ONLY, + immediateOrCancel: true, + }); + await takerDriftClientUser.fetchAccounts(); + + const takerPos = takerDriftClientUser.getPerpPosition(0); + console.log( + 'takerPos.baseAssetAmount:', + takerPos.baseAssetAmount.toString() + ); + assert(takerPos.baseAssetAmount.eq(ZERO)); + + const secondUserSpot = (await secondUserDriftClient.getUserAccount()) + .spotPositions[0]; + console.log('secondUserDriftClient spot 0:', secondUserSpot); + assert(isVariant(secondUserSpot.balanceType, 'deposit')); + console.log( + 'maker token amount:', + secondUserDriftClient.getUser().getTokenAmount(0).toString() + ); + + const txSig = await secondUserDriftClient.placeAndMakePerpOrder( + makerOrderParams, + { + taker: await firstUserDriftClient.getUserAccountPublicKey(), + order: firstUserDriftClient.getOrderByUserId(1), + takerUserAccount: firstUserDriftClient.getUserAccount(), + takerStats: firstUserDriftClient.getUserStatsAccountPublicKey(), + } + ); + + await printTxLogs(connection, txSig); + + await takerDriftClientUser.fetchAccounts(); + + const takerPos2 = takerDriftClientUser.getPerpPosition(0); + console.log( + 'takerPos.baseAssetAmount after:', + takerPos2.baseAssetAmount.toString() + ); + assert(takerPos2.baseAssetAmount.gt(ZERO)); + + const takerUSDCBefore = takerDriftClientUser.getTokenAmount(0); + // const takerSOLBefore = takerDriftClientUser.getTokenAmount(1); + + const makerUSDCBefore = secondUserDriftClient.getUser().getTokenAmount(0); + // const makerSOLBefore = secondUserDriftClient.getUser().getTokenAmount(1); + + //ensure that borrow cant borrow more to settle pnl + console.log('set pyth price to 32.99'); + await setFeedPrice(anchor.workspace.Pyth, 32.99, solOracle); + await firstUserDriftClient.fetchAccounts(); + await secondUserDriftClient.fetchAccounts(); + + // settle losing short maker (who has usdc deposit) first + const settleTx2 = await firstUserDriftClient.settlePNL( + await secondUserDriftClient.getUserAccountPublicKey(), + secondUserDriftClient.getUserAccount(), + marketIndex + ); + await printTxLogs(connection, settleTx2); + + const settleTx1 = await firstUserDriftClient.settlePNL( + await firstUserDriftClient.getUserAccountPublicKey(), + firstUserDriftClient.getUserAccount(), + marketIndex + ); + await printTxLogs(connection, settleTx1); + await secondUserDriftClient.fetchAccounts(); + + const takerUSDCAfter = takerDriftClientUser.getTokenAmount(0); + // const takerSOLAfter = takerDriftClientUser.getTokenAmount(1); + + const makerUSDCAfter = secondUserDriftClient.getUser().getTokenAmount(0); + const solPerpMarketAfter = secondUserDriftClient.getPerpMarketAccount(0); + console.log( + 'solPerpMarketAfter.pnlPool.scaledBalance:', + solPerpMarketAfter.pnlPool.scaledBalance + ); + assert(solPerpMarketAfter.pnlPool.scaledBalance.eq(ZERO)); + // const makerSOLAfter = secondUserDriftClient.getUser().getTokenAmount(1); + + assert(makerUSDCBefore.gt(makerUSDCAfter)); + assert(makerUSDCAfter.eq(ZERO)); + assert(takerUSDCBefore.lte(takerUSDCAfter)); //todo + + //allow that deposit to settle negative pnl for borrow + console.log('set pyth price to 27.4'); + await setFeedPrice(anchor.workspace.Pyth, 27.4, solOracle); + await firstUserDriftClient.fetchAccounts(); + await secondUserDriftClient.fetchAccounts(); + + const settleTx1Good = await firstUserDriftClient.settlePNL( + await firstUserDriftClient.getUserAccountPublicKey(), + firstUserDriftClient.getUserAccount(), + marketIndex + ); + await printTxLogs(connection, settleTx1Good); + + const settleTx2Good = await firstUserDriftClient.settlePNL( + await secondUserDriftClient.getUserAccountPublicKey(), + secondUserDriftClient.getUserAccount(), + marketIndex + ); + await printTxLogs(connection, settleTx2Good); + }); +});